mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 13:21:05 +00:00
Bug 892114: Add support for Unix domain sockets to nsIServerSocket.idl and @mozilla.org/network/server-socket;1. r=mayhemer
This commit is contained in:
parent
b8289518e2
commit
c0a631fad9
@ -4,6 +4,7 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "nsISupports.idl"
|
||||
#include "nsIFile.idl"
|
||||
|
||||
interface nsIServerSocketListener;
|
||||
interface nsISocketTransport;
|
||||
@ -18,7 +19,7 @@ typedef unsigned long nsServerSocketFlag;
|
||||
*
|
||||
* An interface to a server socket that can accept incoming connections.
|
||||
*/
|
||||
[scriptable, uuid(0df6a0e2-a6b1-4d4c-b30d-f2cb093444e3)]
|
||||
[scriptable, uuid(7a9c39cb-a13f-4eef-9bdf-a74301628742)]
|
||||
interface nsIServerSocket : nsISupports
|
||||
{
|
||||
/**
|
||||
@ -94,6 +95,61 @@ interface nsIServerSocket : nsISupports
|
||||
*/
|
||||
[noscript] void initWithAddress([const] in PRNetAddrPtr aAddr, in long aBackLog);
|
||||
|
||||
/**
|
||||
* initWithFilename
|
||||
*
|
||||
* This method initializes a Unix domain or "local" server socket. Such
|
||||
* a socket has a name in the filesystem, like an ordinary file. To
|
||||
* connect, a client supplies the socket's filename, and the usual
|
||||
* permission checks on socket apply.
|
||||
*
|
||||
* This makes Unix domain sockets useful for communication between the
|
||||
* programs being run by a specific user on a single machine: the
|
||||
* operating system takes care of authentication, and the user's home
|
||||
* directory or profile directory provide natural per-user rendezvous
|
||||
* points.
|
||||
*
|
||||
* Since Unix domain sockets are always local to the machine, they are
|
||||
* not affected by the nsIIOService's 'offline' flag.
|
||||
*
|
||||
* The system-level socket API may impose restrictions on the length of
|
||||
* the filename that are stricter than those of the underlying
|
||||
* filesystem. If the file name is too long, this returns
|
||||
* NS_ERROR_FILE_NAME_TOO_LONG.
|
||||
*
|
||||
* All components of the path prefix of |aPath| must name directories;
|
||||
* otherwise, this returns NS_ERROR_FILE_NOT_DIRECTORY.
|
||||
*
|
||||
* This call requires execute permission on all directories containing
|
||||
* the one in which the socket is to be created, and write and execute
|
||||
* permission on the directory itself. Otherwise, this returns
|
||||
* NS_ERROR_CONNECTION_REFUSED.
|
||||
*
|
||||
* This call creates the socket's directory entry. There must not be
|
||||
* any existing entry with the given name. If there is, this returns
|
||||
* NS_ERROR_SOCKET_ADDRESS_IN_USE.
|
||||
*
|
||||
* On systems that don't support Unix domain sockets at all, this
|
||||
* returns NS_ERROR_SOCKET_ADDRESS_NOT_SUPPORTED.
|
||||
*
|
||||
* @param aPath nsIFile
|
||||
* The file name at which the socket should be created.
|
||||
*
|
||||
* @param aPermissions unsigned long
|
||||
* Unix-style permission bits to be applied to the new socket.
|
||||
*
|
||||
* Note about permissions: Linux's unix(7) man page claims that some
|
||||
* BSD-derived systems ignore permissions on UNIX-domain sockets;
|
||||
* NetBSD's bind(2) man page agrees, but says it does check now (dated
|
||||
* 2005). POSIX has required 'connect' to fail if write permission on
|
||||
* the socket itself is not granted since 2003 (Issue 6). NetBSD says
|
||||
* that the permissions on the containing directory (execute) have
|
||||
* always applied, so creating sockets in appropriately protected
|
||||
* directories should be secure on both old and new systems.
|
||||
*/
|
||||
void initWithFilename(in nsIFile aPath, in unsigned long aPermissions,
|
||||
in long aBacklog);
|
||||
|
||||
/**
|
||||
* close
|
||||
*
|
||||
|
@ -27,12 +27,15 @@ native NetAddr(mozilla::net::NetAddr);
|
||||
interface nsISocketTransport : nsITransport
|
||||
{
|
||||
/**
|
||||
* Get the host for the underlying socket connection.
|
||||
* Get the peer's host for the underlying socket connection.
|
||||
* For Unix domain sockets, this is a pathname, or the empty string for
|
||||
* unnamed and abstract socket addresses.
|
||||
*/
|
||||
readonly attribute AUTF8String host;
|
||||
|
||||
/**
|
||||
* Get the port for the underlying socket connection.
|
||||
* For Unix domain sockets, this is zero.
|
||||
*/
|
||||
readonly attribute long port;
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "nsISupports.idl"
|
||||
#include "nsIFile.idl"
|
||||
|
||||
interface nsISocketTransport;
|
||||
interface nsIProxyInfo;
|
||||
@ -17,7 +18,7 @@ struct PRFileDesc;
|
||||
[ptr] native PRFileDescPtr(PRFileDesc);
|
||||
[ptr] native nsASocketHandlerPtr(nsASocketHandler);
|
||||
|
||||
[scriptable, uuid(185B3A5D-8729-436D-9693-7BDCCB9C2216)]
|
||||
[scriptable, uuid(ad56b25f-e6bb-4db3-9f7b-5b7db33fd2b1)]
|
||||
interface nsISocketTransportService : nsISupports
|
||||
{
|
||||
/**
|
||||
@ -49,6 +50,40 @@ interface nsISocketTransportService : nsISupports
|
||||
in long aPort,
|
||||
in nsIProxyInfo aProxyInfo);
|
||||
|
||||
/**
|
||||
* Create a transport built on a Unix domain socket, connecting to the
|
||||
* given filename.
|
||||
*
|
||||
* Since Unix domain sockets are always local to the machine, they are
|
||||
* not affected by the nsIIOService's 'offline' flag.
|
||||
*
|
||||
* On systems that don't support Unix domain sockets at all, this
|
||||
* returns NS_ERROR_SOCKET_ADDRESS_NOT_SUPPORTED.
|
||||
*
|
||||
* The system-level socket API may impose restrictions on the length of
|
||||
* the filename that are stricter than those of the underlying
|
||||
* filesystem. If the file name is too long, this returns
|
||||
* NS_ERROR_FILE_NAME_TOO_LONG.
|
||||
*
|
||||
* The |aPath| parameter must specify an existing directory entry.
|
||||
* Otherwise, this returns NS_ERROR_FILE_NOT_FOUND.
|
||||
*
|
||||
* The program must have search permission on all components of the
|
||||
* path prefix of |aPath|, and read and write permission on |aPath|
|
||||
* itself. Without such permission, this returns
|
||||
* NS_ERROR_CONNECTION_REFUSED.
|
||||
*
|
||||
* The |aPath| parameter must refer to a unix-domain socket. Otherwise,
|
||||
* this returns NS_ERROR_CONNECTION_REFUSED. (POSIX specifies
|
||||
* ECONNREFUSED when "the target address was not listening for
|
||||
* connections", and this is what Linux returns.)
|
||||
*
|
||||
* @param aPath
|
||||
* The file name of the Unix domain socket to which we should
|
||||
* connect.
|
||||
*/
|
||||
nsISocketTransport createUnixDomainTransport(in nsIFile aPath);
|
||||
|
||||
/**
|
||||
* Adds a new socket to the list of controlled sockets.
|
||||
*
|
||||
|
@ -173,6 +173,13 @@ nsServerSocket::OnSocketReady(PRFileDesc *fd, int16_t outFlags)
|
||||
PRNetAddr prClientAddr;
|
||||
NetAddr clientAddr;
|
||||
|
||||
// NSPR doesn't tell us the peer address's length (as provided by the
|
||||
// 'accept' system call), so we can't distinguish between named,
|
||||
// unnamed, and abstract peer addresses. Clear prClientAddr first, so
|
||||
// that the path will at least be reliably empty for unnamed and
|
||||
// abstract addresses, and not garbage when the peer is unnamed.
|
||||
memset(&prClientAddr, 0, sizeof(prClientAddr));
|
||||
|
||||
clientFD = PR_Accept(mFD, &prClientAddr, PR_INTERVAL_NO_WAIT);
|
||||
PRNetAddrToNetAddr(&prClientAddr, &clientAddr);
|
||||
if (!clientFD)
|
||||
@ -230,6 +237,15 @@ nsServerSocket::OnSocketDetached(PRFileDesc *fd)
|
||||
void
|
||||
nsServerSocket::IsLocal(bool *aIsLocal)
|
||||
{
|
||||
#if defined(XP_UNIX) || defined(XP_OS2)
|
||||
// Unix-domain sockets are always local.
|
||||
if (mAddr.raw.family == PR_AF_LOCAL)
|
||||
{
|
||||
*aIsLocal = true;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// If bound to loopback, this server socket only accepts local connections.
|
||||
*aIsLocal = PR_IsNetAddrType(&mAddr, PR_IpAddrLoopback);
|
||||
}
|
||||
@ -257,6 +273,35 @@ nsServerSocket::Init(int32_t aPort, bool aLoopbackOnly, int32_t aBackLog)
|
||||
return InitSpecialConnection(aPort, aLoopbackOnly ? LoopbackOnly : 0, aBackLog);
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsServerSocket::InitWithFilename(nsIFile *aPath, uint32_t aPermissions, int32_t aBacklog)
|
||||
{
|
||||
#if defined(XP_UNIX) || defined(XP_OS2)
|
||||
nsresult rv;
|
||||
|
||||
nsAutoCString path;
|
||||
rv = aPath->GetNativePath(path);
|
||||
if (NS_FAILED(rv))
|
||||
return rv;
|
||||
|
||||
// Create a Unix domain PRNetAddr referring to the given path.
|
||||
PRNetAddr addr;
|
||||
if (path.Length() > sizeof(addr.local.path) - 1)
|
||||
return NS_ERROR_FILE_NAME_TOO_LONG;
|
||||
addr.local.family = PR_AF_LOCAL;
|
||||
memcpy(addr.local.path, path.get(), path.Length());
|
||||
addr.local.path[path.Length()] = '\0';
|
||||
|
||||
rv = InitWithAddress(&addr, aBacklog);
|
||||
if (NS_FAILED(rv))
|
||||
return rv;
|
||||
|
||||
return aPath->SetPermissions(aPermissions);
|
||||
#else
|
||||
return NS_ERROR_SOCKET_ADDRESS_NOT_SUPPORTED;
|
||||
#endif
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsServerSocket::InitSpecialConnection(int32_t aPort, nsServerSocketFlag aFlags,
|
||||
int32_t aBackLog)
|
||||
@ -469,8 +514,11 @@ nsServerSocket::GetPort(int32_t *aResult)
|
||||
uint16_t port;
|
||||
if (mAddr.raw.family == PR_AF_INET)
|
||||
port = mAddr.inet.port;
|
||||
else
|
||||
else if (mAddr.raw.family == PR_AF_INET6)
|
||||
port = mAddr.ipv6.port;
|
||||
else
|
||||
return NS_ERROR_FAILURE;
|
||||
|
||||
*aResult = (int32_t) PR_ntohs(port);
|
||||
return NS_OK;
|
||||
}
|
||||
|
@ -863,20 +863,46 @@ nsSocketTransport::Init(const char **types, uint32_t typeCount,
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult
|
||||
nsSocketTransport::InitWithFilename(const char *filename)
|
||||
{
|
||||
#if defined(XP_UNIX) || defined(XP_OS2)
|
||||
size_t filenameLength = strlen(filename);
|
||||
|
||||
if (filenameLength > sizeof(mNetAddr.local.path) - 1)
|
||||
return NS_ERROR_FILE_NAME_TOO_LONG;
|
||||
|
||||
mHost.Assign(filename);
|
||||
mPort = 0;
|
||||
mTypeCount = 0;
|
||||
|
||||
mNetAddr.local.family = AF_LOCAL;
|
||||
memcpy(mNetAddr.local.path, filename, filenameLength);
|
||||
mNetAddr.local.path[filenameLength] = '\0';
|
||||
mNetAddrIsSet = true;
|
||||
|
||||
return NS_OK;
|
||||
#else
|
||||
return NS_ERROR_SOCKET_ADDRESS_NOT_SUPPORTED;
|
||||
#endif
|
||||
}
|
||||
|
||||
nsresult
|
||||
nsSocketTransport::InitWithConnectedSocket(PRFileDesc *fd, const NetAddr *addr)
|
||||
{
|
||||
NS_ASSERTION(!mFD, "already initialized");
|
||||
|
||||
char buf[kIPv6CStrBufSize];
|
||||
char buf[kNetAddrMaxCStrBufSize];
|
||||
NetAddrToString(addr, buf, sizeof(buf));
|
||||
mHost.Assign(buf);
|
||||
|
||||
uint16_t port;
|
||||
if (addr->raw.family == AF_INET)
|
||||
port = addr->inet.port;
|
||||
else
|
||||
else if (addr->raw.family == AF_INET6)
|
||||
port = addr->inet6.port;
|
||||
else
|
||||
port = 0;
|
||||
mPort = ntohs(port);
|
||||
|
||||
memcpy(&mNetAddr, addr, sizeof(NetAddr));
|
||||
@ -951,6 +977,10 @@ nsSocketTransport::ResolveHost()
|
||||
|
||||
if (!mProxyHost.IsEmpty()) {
|
||||
if (!mProxyTransparent || mProxyTransparentResolvesHost) {
|
||||
#if defined(XP_UNIX) || defined(XP_OS2)
|
||||
NS_ABORT_IF_FALSE(!mNetAddrIsSet || mNetAddr.raw.family != AF_LOCAL,
|
||||
"Unix domain sockets can't be used with proxies");
|
||||
#endif
|
||||
// When not resolving mHost locally, we still want to ensure that
|
||||
// it only contains valid characters. See bug 304904 for details.
|
||||
if (!net_IsValidHostName(mHost))
|
||||
@ -1014,6 +1044,11 @@ nsSocketTransport::BuildSocket(PRFileDesc *&fd, bool &proxyTransparent, bool &us
|
||||
rv = fd ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
else {
|
||||
#if defined(XP_UNIX) || defined(XP_OS2)
|
||||
NS_ABORT_IF_FALSE(!mNetAddrIsSet || mNetAddr.raw.family != AF_LOCAL,
|
||||
"Unix domain sockets can't be used with socket types");
|
||||
#endif
|
||||
|
||||
fd = nullptr;
|
||||
|
||||
nsCOMPtr<nsISocketProviderService> spserv =
|
||||
@ -1117,9 +1152,13 @@ nsSocketTransport::InitiateSocket()
|
||||
|
||||
nsresult rv;
|
||||
|
||||
if (gIOService->IsOffline() &&
|
||||
!IsLoopBackAddress(&mNetAddr))
|
||||
return NS_ERROR_OFFLINE;
|
||||
if (gIOService->IsOffline()) {
|
||||
bool isLocal;
|
||||
|
||||
IsLocal(&isLocal);
|
||||
if (!isLocal)
|
||||
return NS_ERROR_OFFLINE;
|
||||
}
|
||||
|
||||
//
|
||||
// find out if it is going to be ok to attach another socket to the STS.
|
||||
@ -1224,7 +1263,7 @@ nsSocketTransport::InitiateSocket()
|
||||
|
||||
#if defined(PR_LOGGING)
|
||||
if (SOCKET_LOG_ENABLED()) {
|
||||
char buf[kIPv6CStrBufSize];
|
||||
char buf[kNetAddrMaxCStrBufSize];
|
||||
NetAddrToString(&mNetAddr, buf, sizeof(buf));
|
||||
SOCKET_LOG((" trying address: %s\n", buf));
|
||||
}
|
||||
@ -1312,6 +1351,13 @@ nsSocketTransport::RecoverFromError()
|
||||
SOCKET_LOG(("nsSocketTransport::RecoverFromError [this=%p state=%x cond=%x]\n",
|
||||
this, mState, mCondition));
|
||||
|
||||
#if defined(XP_UNIX) || defined(XP_OS2)
|
||||
// Unix domain connections don't have multiple addresses to try,
|
||||
// so the recovery techniques here don't apply.
|
||||
if (mNetAddrIsSet && mNetAddr.raw.family == AF_LOCAL)
|
||||
return false;
|
||||
#endif
|
||||
|
||||
// can only recover from errors in these states
|
||||
if (mState != STATE_RESOLVING && mState != STATE_CONNECTING)
|
||||
return false;
|
||||
@ -1516,6 +1562,7 @@ void
|
||||
nsSocketTransport::ReleaseFD_Locked(PRFileDesc *fd)
|
||||
{
|
||||
NS_ASSERTION(mFD == fd, "wrong fd");
|
||||
SOCKET_LOG(("JIMB: ReleaseFD_Locked: mFDref = %d\n", mFDref));
|
||||
|
||||
if (--mFDref == 0) {
|
||||
if (PR_GetCurrentThread() == gSocketThread) {
|
||||
@ -1556,9 +1603,18 @@ nsSocketTransport::OnSocketEvent(uint32_t type, nsresult status, nsISupports *pa
|
||||
// ensure that we have created a socket, attached it, and have a
|
||||
// connection.
|
||||
//
|
||||
if (mState == STATE_CLOSED)
|
||||
mCondition = ResolveHost();
|
||||
else
|
||||
if (mState == STATE_CLOSED) {
|
||||
// Unix domain sockets are ready to connect; mNetAddr is all we
|
||||
// need. Internet address families require a DNS lookup (or possibly
|
||||
// several) before we can connect.
|
||||
#if defined(XP_UNIX) || defined(XP_OS2)
|
||||
if (mNetAddrIsSet && mNetAddr.raw.family == AF_LOCAL)
|
||||
mCondition = InitiateSocket();
|
||||
else
|
||||
#endif
|
||||
mCondition = ResolveHost();
|
||||
|
||||
} else
|
||||
SOCKET_LOG((" ignoring redundant event\n"));
|
||||
break;
|
||||
|
||||
@ -1789,6 +1845,16 @@ nsSocketTransport::IsLocal(bool *aIsLocal)
|
||||
{
|
||||
{
|
||||
MutexAutoLock lock(mLock);
|
||||
|
||||
#if defined(XP_UNIX) || defined(XP_OS2)
|
||||
// Unix-domain sockets are always local.
|
||||
if (mNetAddr.raw.family == PR_AF_LOCAL)
|
||||
{
|
||||
*aIsLocal = true;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
*aIsLocal = IsLoopBackAddress(&mNetAddr);
|
||||
}
|
||||
}
|
||||
@ -2050,6 +2116,16 @@ nsSocketTransport::GetSelfAddr(NetAddr *addr)
|
||||
}
|
||||
|
||||
PRNetAddr prAddr;
|
||||
|
||||
// NSPR doesn't tell us the socket address's length (as provided by
|
||||
// the 'getsockname' system call), so we can't distinguish between
|
||||
// named, unnamed, and abstract Unix domain socket names. (Server
|
||||
// sockets are never unnamed, obviously, but client sockets can use
|
||||
// any kind of address.) Clear prAddr first, so that the path for
|
||||
// unnamed and abstract addresses will at least be reliably empty,
|
||||
// and not garbage for unnamed sockets.
|
||||
memset(&prAddr, 0, sizeof(prAddr));
|
||||
|
||||
nsresult rv =
|
||||
(PR_GetSockName(fd, &prAddr) == PR_SUCCESS) ? NS_OK : NS_ERROR_FAILURE;
|
||||
PRNetAddrToNetAddr(&prAddr, addr);
|
||||
|
@ -129,6 +129,11 @@ public:
|
||||
nsresult InitWithConnectedSocket(PRFileDesc *socketFD,
|
||||
const mozilla::net::NetAddr *addr);
|
||||
|
||||
// This method instructs the socket transport to open a socket
|
||||
// connected to the given Unix domain address. We can only create
|
||||
// unlayered, simple, stream sockets.
|
||||
nsresult InitWithFilename(const char *filename);
|
||||
|
||||
// nsASocketHandler methods:
|
||||
void OnSocketReady(PRFileDesc *, int16_t outFlags);
|
||||
void OnSocketDetached(PRFileDesc *);
|
||||
|
@ -573,6 +573,31 @@ nsSocketTransportService::CreateTransport(const char **types,
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsSocketTransportService::CreateUnixDomainTransport(nsIFile *aPath,
|
||||
nsISocketTransport **result)
|
||||
{
|
||||
nsresult rv;
|
||||
|
||||
NS_ENSURE_TRUE(mInitialized, NS_ERROR_NOT_INITIALIZED);
|
||||
|
||||
nsAutoCString path;
|
||||
rv = aPath->GetNativePath(path);
|
||||
if (NS_FAILED(rv))
|
||||
return rv;
|
||||
|
||||
nsRefPtr<nsSocketTransport> trans = new nsSocketTransport();
|
||||
if (!trans)
|
||||
return NS_ERROR_OUT_OF_MEMORY;
|
||||
|
||||
rv = trans->InitWithFilename(path.get());
|
||||
if (NS_FAILED(rv))
|
||||
return rv;
|
||||
|
||||
trans.forget(result);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsSocketTransportService::GetAutodialEnabled(bool *value)
|
||||
{
|
||||
|
@ -115,9 +115,20 @@ bool NetAddrToString(const NetAddr *addr, char *buf, uint32_t bufSize)
|
||||
#if defined(XP_UNIX) || defined(XP_OS2)
|
||||
else if (addr->raw.family == AF_LOCAL) {
|
||||
if (bufSize < sizeof(addr->local.path)) {
|
||||
// Many callers don't bother checking our return value, so
|
||||
// null-terminate just in case.
|
||||
if (bufSize > 0) {
|
||||
buf[0] = '\0';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
memcpy(buf, addr->local.path, bufSize);
|
||||
|
||||
// Usually, the size passed to memcpy should be the size of the
|
||||
// destination. Here, we know that the source is no larger than the
|
||||
// destination, so using the source's size is always safe, whereas
|
||||
// using the destination's size may cause us to read off the end of the
|
||||
// source.
|
||||
memcpy(buf, addr->local.path, sizeof(addr->local.path));
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
@ -55,9 +55,12 @@ namespace net {
|
||||
// Windows requires longer buffers for some reason.
|
||||
const int kIPv4CStrBufSize = 22;
|
||||
const int kIPv6CStrBufSize = 65;
|
||||
const int kNetAddrMaxCStrBufSize = kIPv6CStrBufSize;
|
||||
#else
|
||||
const int kIPv4CStrBufSize = 16;
|
||||
const int kIPv6CStrBufSize = 46;
|
||||
const int kLocalCStrBufSize = 108;
|
||||
const int kNetAddrMaxCStrBufSize = kLocalCStrBufSize;
|
||||
#endif
|
||||
|
||||
// This was all created at a time in which we were using NSPR for host
|
||||
|
533
netwerk/test/unit/test_unix_domain.js
Normal file
533
netwerk/test/unit/test_unix_domain.js
Normal file
@ -0,0 +1,533 @@
|
||||
// Exercise Unix domain sockets.
|
||||
|
||||
const Ci = Components.interfaces;
|
||||
const Cc = Components.classes;
|
||||
const CC = Components.Constructor;
|
||||
const Cr = Components.results;
|
||||
|
||||
const UnixServerSocket = CC("@mozilla.org/network/server-socket;1",
|
||||
"nsIServerSocket",
|
||||
"initWithFilename");
|
||||
|
||||
const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1",
|
||||
"nsIScriptableInputStream",
|
||||
"init");
|
||||
|
||||
const IOService = Cc["@mozilla.org/network/io-service;1"]
|
||||
.getService(Ci.nsIIOService);
|
||||
const socketTransportService = Cc["@mozilla.org/network/socket-transport-service;1"]
|
||||
.getService(Ci.nsISocketTransportService);
|
||||
|
||||
const threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
|
||||
|
||||
const allPermissions = parseInt("777", 8);
|
||||
|
||||
function run_test()
|
||||
{
|
||||
// If we're on Windows, simply check for graceful failure.
|
||||
if ("@mozilla.org/windows-registry-key;1" in Cc) {
|
||||
test_not_supported();
|
||||
return;
|
||||
}
|
||||
|
||||
add_test(test_echo);
|
||||
add_test(test_name_too_long);
|
||||
add_test(test_no_directory);
|
||||
add_test(test_no_such_socket);
|
||||
add_test(test_address_in_use);
|
||||
add_test(test_file_in_way);
|
||||
add_test(test_create_permission);
|
||||
add_test(test_connect_permission);
|
||||
add_test(test_long_socket_name);
|
||||
add_test(test_keep_when_offline);
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
// Check that creating a Unix domain socket fails gracefully on Windows.
|
||||
function test_not_supported()
|
||||
{
|
||||
let socketName = do_get_tempdir();
|
||||
socketName.append('socket');
|
||||
do_print("creating socket: " + socketName.path);
|
||||
|
||||
do_check_throws_nsIException(() => new UnixServerSocket(socketName, allPermissions, -1),
|
||||
"NS_ERROR_SOCKET_ADDRESS_NOT_SUPPORTED");
|
||||
|
||||
do_check_throws_nsIException(() => socketTransportService.createUnixDomainTransport(socketName),
|
||||
"NS_ERROR_SOCKET_ADDRESS_NOT_SUPPORTED");
|
||||
}
|
||||
|
||||
// Actually exchange data with Unix domain sockets.
|
||||
function test_echo()
|
||||
{
|
||||
let log = '';
|
||||
|
||||
let socketName = do_get_tempdir();
|
||||
socketName.append('socket');
|
||||
|
||||
// Create a server socket, listening for connections.
|
||||
do_print("creating socket: " + socketName.path);
|
||||
let server = new UnixServerSocket(socketName, allPermissions, -1);
|
||||
server.asyncListen({
|
||||
onSocketAccepted: function(aServ, aTransport) {
|
||||
do_print("called test_echo's onSocketAccepted");
|
||||
log += 'a';
|
||||
|
||||
do_check_eq(aServ, server);
|
||||
|
||||
let connection = aTransport;
|
||||
|
||||
// Check the server socket's self address.
|
||||
let connectionSelfAddr = connection.getScriptableSelfAddr();
|
||||
do_check_eq(connectionSelfAddr.family, Ci.nsINetAddr.FAMILY_LOCAL);
|
||||
do_check_eq(connectionSelfAddr.address, socketName.path);
|
||||
|
||||
// The client socket is anonymous, so the server transport should
|
||||
// have an empty peer address.
|
||||
do_check_eq(connection.host, '');
|
||||
do_check_eq(connection.port, 0);
|
||||
let connectionPeerAddr = connection.getScriptablePeerAddr();
|
||||
do_check_eq(connectionPeerAddr.family, Ci.nsINetAddr.FAMILY_LOCAL);
|
||||
do_check_eq(connectionPeerAddr.address, '');
|
||||
|
||||
let serverInput = new ScriptableInputStream(connection.openInputStream(0, 0, 0));
|
||||
let serverOutput = connection.openOutputStream(0, 0, 0);
|
||||
|
||||
// Receive data from the client, and send back a response.
|
||||
do_check_eq(serverInput.readBytes(17), "Mervyn Murgatroyd");
|
||||
serverOutput.write("Ruthven Murgatroyd", 18);
|
||||
},
|
||||
|
||||
onStopListening: function(aServ, aStatus) {
|
||||
do_print("called test_echo's onStopListening");
|
||||
log += 's';
|
||||
|
||||
do_check_eq(aServ, server);
|
||||
do_check_eq(log, 'acs');
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
});
|
||||
|
||||
// Create a client socket, and connect to the server.
|
||||
let client = socketTransportService.createUnixDomainTransport(socketName);
|
||||
do_check_eq(client.host, socketName.path);
|
||||
do_check_eq(client.port, 0);
|
||||
|
||||
let clientAsyncInput = client.openInputStream(0, 0, 0).QueryInterface(Ci.nsIAsyncInputStream);
|
||||
let clientInput = new ScriptableInputStream(clientAsyncInput);
|
||||
let clientOutput = client.openOutputStream(0, 0, 0);
|
||||
|
||||
clientOutput.write("Mervyn Murgatroyd", 17);
|
||||
clientAsyncInput.asyncWait(function (aStream) {
|
||||
do_print("called test_echo's onInputStreamReady");
|
||||
log += 'c';
|
||||
|
||||
do_check_eq(aStream, clientAsyncInput);
|
||||
|
||||
// Now that the connection has been established, we can check the
|
||||
// transport's self and peer addresses.
|
||||
let clientSelfAddr = client.getScriptableSelfAddr();
|
||||
do_check_eq(clientSelfAddr.family, Ci.nsINetAddr.FAMILY_LOCAL);
|
||||
do_check_eq(clientSelfAddr.address, '');
|
||||
|
||||
do_check_eq(client.host, socketName.path); // re-check, but hey
|
||||
let clientPeerAddr = client.getScriptablePeerAddr();
|
||||
do_check_eq(clientPeerAddr.family, Ci.nsINetAddr.FAMILY_LOCAL);
|
||||
do_check_eq(clientPeerAddr.address, socketName.path);
|
||||
|
||||
do_check_eq(clientInput.readBytes(18), "Ruthven Murgatroyd");
|
||||
|
||||
server.close();
|
||||
}, 0, 0, threadManager.currentThread);
|
||||
}
|
||||
|
||||
// Create client and server sockets using a path that's too long.
|
||||
function test_name_too_long()
|
||||
{
|
||||
let socketName = do_get_tempdir();
|
||||
// The length limits on all the systems NSPR supports are a bit past 100.
|
||||
socketName.append(new Array(1000).join('x'));
|
||||
|
||||
// The length must be checked before we ever make any system calls --- we
|
||||
// have to create the sockaddr first --- so it's unambiguous which error
|
||||
// we should get here.
|
||||
|
||||
do_check_throws_nsIException(() => new UnixServerSocket(socketName, 0, -1),
|
||||
"NS_ERROR_FILE_NAME_TOO_LONG");
|
||||
|
||||
// Unlike most other client socket errors, this one gets reported
|
||||
// immediately, as we can't even initialize the sockaddr with the given
|
||||
// name.
|
||||
do_check_throws_nsIException(() => socketTransportService.createUnixDomainTransport(socketName),
|
||||
"NS_ERROR_FILE_NAME_TOO_LONG");
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
// Try creating a socket in a directory that doesn't exist.
|
||||
function test_no_directory()
|
||||
{
|
||||
let socketName = do_get_tempdir();
|
||||
socketName.append('directory-that-does-not-exist');
|
||||
socketName.append('socket');
|
||||
|
||||
do_check_throws_nsIException(() => new UnixServerSocket(socketName, 0, -1),
|
||||
"NS_ERROR_FILE_NOT_FOUND");
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
// Try connecting to a server socket that isn't there.
|
||||
function test_no_such_socket()
|
||||
{
|
||||
let socketName = do_get_tempdir();
|
||||
socketName.append('nonexistent-socket');
|
||||
|
||||
let client = socketTransportService.createUnixDomainTransport(socketName);
|
||||
let clientAsyncInput = client.openInputStream(0, 0, 0).QueryInterface(Ci.nsIAsyncInputStream);
|
||||
clientAsyncInput.asyncWait(function (aStream) {
|
||||
do_print("called test_no_such_socket's onInputStreamReady");
|
||||
|
||||
do_check_eq(aStream, clientAsyncInput);
|
||||
|
||||
// nsISocketTransport puts off actually creating sockets as long as
|
||||
// possible, so the error in connecting doesn't actually show up until
|
||||
// this point.
|
||||
do_check_throws_nsIException(() => clientAsyncInput.available(),
|
||||
"NS_ERROR_FILE_NOT_FOUND");
|
||||
|
||||
clientAsyncInput.close();
|
||||
client.close(Cr.NS_OK);
|
||||
|
||||
run_next_test();
|
||||
}, 0, 0, threadManager.currentThread);
|
||||
}
|
||||
|
||||
// Creating a socket with a name that another socket is already using is an
|
||||
// error.
|
||||
function test_address_in_use()
|
||||
{
|
||||
let socketName = do_get_tempdir();
|
||||
socketName.append('socket-in-use');
|
||||
|
||||
// Create one server socket.
|
||||
let server = new UnixServerSocket(socketName, allPermissions, -1);
|
||||
|
||||
// Now try to create another with the same name.
|
||||
do_check_throws_nsIException(() => new UnixServerSocket(socketName, allPermissions, -1),
|
||||
"NS_ERROR_SOCKET_ADDRESS_IN_USE");
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
// Creating a socket with a name that is already a file is an error.
|
||||
function test_file_in_way()
|
||||
{
|
||||
let socketName = do_get_tempdir();
|
||||
socketName.append('file_in_way');
|
||||
|
||||
// Create a file with the given name.
|
||||
socketName.create(Ci.nsIFile.NORMAL_FILE_TYPE, allPermissions);
|
||||
|
||||
// Try to create a socket with the same name.
|
||||
do_check_throws_nsIException(() => new UnixServerSocket(socketName, allPermissions, -1),
|
||||
"NS_ERROR_SOCKET_ADDRESS_IN_USE");
|
||||
|
||||
// Try to create a socket under a name that uses that as a parent directory.
|
||||
socketName.append('socket');
|
||||
do_check_throws_nsIException(() => new UnixServerSocket(socketName, 0, -1),
|
||||
"NS_ERROR_FILE_NOT_DIRECTORY");
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
// It is not permitted to create a socket in a directory which we are not
|
||||
// permitted to execute, or create files in.
|
||||
function test_create_permission()
|
||||
{
|
||||
let dirName = do_get_tempdir();
|
||||
dirName.append('unfriendly');
|
||||
|
||||
let socketName = dirName.clone();
|
||||
socketName.append('socket');
|
||||
|
||||
// The test harness has difficulty cleaning things up if we don't make
|
||||
// everything writable before we're done.
|
||||
try {
|
||||
// Create a directory which we are not permitted to search.
|
||||
dirName.create(Ci.nsIFile.DIRECTORY_TYPE, 0);
|
||||
|
||||
// Try to create a socket in that directory. Because Linux returns EACCES
|
||||
// when a 'connect' fails because of a local firewall rule,
|
||||
// nsIServerSocket returns NS_ERROR_CONNECTION_REFUSED in this case.
|
||||
do_check_throws_nsIException(() => new UnixServerSocket(socketName, allPermissions, -1),
|
||||
"NS_ERROR_CONNECTION_REFUSED");
|
||||
|
||||
// Grant read and execute permission, but not write permission on the directory.
|
||||
dirName.permissions = parseInt("0555", 8);
|
||||
|
||||
// This should also fail; we need write permission.
|
||||
do_check_throws_nsIException(() => new UnixServerSocket(socketName, allPermissions, -1),
|
||||
"NS_ERROR_CONNECTION_REFUSED");
|
||||
|
||||
} finally {
|
||||
// Make the directory writable, so the test harness can clean it up.
|
||||
dirName.permissions = allPermissions;
|
||||
}
|
||||
|
||||
// This should succeed, since we now have all the permissions on the
|
||||
// directory we could want.
|
||||
do_check_instanceof(new UnixServerSocket(socketName, allPermissions, -1),
|
||||
Ci.nsIServerSocket);
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
// To connect to a Unix domain socket, we need search permission on the
|
||||
// directories containing it, and some kind of permission or other on the
|
||||
// socket itself.
|
||||
function test_connect_permission()
|
||||
{
|
||||
// This test involves a lot of callbacks, but they're written out so that
|
||||
// the actual control flow proceeds from top to bottom.
|
||||
let log = '';
|
||||
|
||||
// Create a directory which we are permitted to search - at first.
|
||||
let dirName = do_get_tempdir();
|
||||
dirName.append('inhospitable');
|
||||
dirName.create(Ci.nsIFile.DIRECTORY_TYPE, allPermissions);
|
||||
|
||||
let socketName = dirName.clone();
|
||||
socketName.append('socket');
|
||||
|
||||
// Create a server socket in that directory, listening for connections,
|
||||
// and accessible.
|
||||
let server = new UnixServerSocket(socketName, allPermissions, -1);
|
||||
server.asyncListen({ onSocketAccepted: socketAccepted, onStopListening: stopListening });
|
||||
|
||||
// Make the directory unsearchable.
|
||||
dirName.permissions = 0;
|
||||
|
||||
let client3;
|
||||
|
||||
let client1 = socketTransportService.createUnixDomainTransport(socketName);
|
||||
let client1AsyncInput = client1.openInputStream(0, 0, 0).QueryInterface(Ci.nsIAsyncInputStream);
|
||||
client1AsyncInput.asyncWait(function (aStream) {
|
||||
do_print("called test_connect_permission's client1's onInputStreamReady");
|
||||
log += '1';
|
||||
|
||||
// nsISocketTransport puts off actually creating sockets as long as
|
||||
// possible, so the error doesn't actually show up until this point.
|
||||
do_check_throws_nsIException(() => client1AsyncInput.available(),
|
||||
"NS_ERROR_CONNECTION_REFUSED");
|
||||
|
||||
client1AsyncInput.close();
|
||||
client1.close(Cr.NS_OK);
|
||||
|
||||
// Make the directory searchable, but make the socket inaccessible.
|
||||
dirName.permissions = allPermissions;
|
||||
socketName.permissions = 0;
|
||||
|
||||
let client2 = socketTransportService.createUnixDomainTransport(socketName);
|
||||
let client2AsyncInput = client2.openInputStream(0, 0, 0).QueryInterface(Ci.nsIAsyncInputStream);
|
||||
client2AsyncInput.asyncWait(function (aStream) {
|
||||
do_print("called test_connect_permission's client2's onInputStreamReady");
|
||||
log += '2';
|
||||
|
||||
do_check_throws_nsIException(() => client2AsyncInput.available(),
|
||||
"NS_ERROR_CONNECTION_REFUSED");
|
||||
|
||||
client2AsyncInput.close();
|
||||
client2.close(Cr.NS_OK);
|
||||
|
||||
// Now make everything accessible, and try one last time.
|
||||
socketName.permissions = allPermissions;
|
||||
|
||||
client3 = socketTransportService.createUnixDomainTransport(socketName);
|
||||
|
||||
let client3Output = client3.openOutputStream(0, 0, 0);
|
||||
client3Output.write("Hanratty", 8);
|
||||
|
||||
let client3AsyncInput = client3.openInputStream(0, 0, 0).QueryInterface(Ci.nsIAsyncInputStream);
|
||||
client3AsyncInput.asyncWait(client3InputStreamReady, 0, 0, threadManager.currentThread);
|
||||
}, 0, 0, threadManager.currentThread);
|
||||
}, 0, 0, threadManager.currentThread);
|
||||
|
||||
function socketAccepted(aServ, aTransport) {
|
||||
do_print("called test_connect_permission's onSocketAccepted");
|
||||
log += 'a';
|
||||
|
||||
let serverInput = new ScriptableInputStream(aTransport.openInputStream(0, 0, 0));
|
||||
let serverOutput = aTransport.openOutputStream(0, 0, 0);
|
||||
|
||||
// Receive data from the client, and send back a response.
|
||||
do_check_eq(serverInput.readBytes(8), "Hanratty");
|
||||
serverOutput.write("Ferlingatti", 11);
|
||||
}
|
||||
|
||||
function client3InputStreamReady(aStream) {
|
||||
do_print("called client3's onInputStreamReady");
|
||||
log += '3';
|
||||
|
||||
let client3Input = new ScriptableInputStream(aStream);
|
||||
|
||||
do_check_eq(client3Input.readBytes(11), "Ferlingatti");
|
||||
|
||||
aStream.close();
|
||||
client3.close(Cr.NS_OK);
|
||||
server.close();
|
||||
}
|
||||
|
||||
function stopListening(aServ, aStatus) {
|
||||
do_print("called test_connect_permission's server's stopListening");
|
||||
log += 's';
|
||||
|
||||
do_check_eq(log, '12a3s');
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
}
|
||||
|
||||
// Creating a socket with a long filename doesn't crash.
|
||||
function test_long_socket_name()
|
||||
{
|
||||
let socketName = do_get_tempdir();
|
||||
socketName.append(new Array(10000).join('long'));
|
||||
|
||||
// Try to create a server socket with the long name.
|
||||
do_check_throws_nsIException(() => new UnixServerSocket(socketName, allPermissions, -1),
|
||||
"NS_ERROR_FILE_NAME_TOO_LONG");
|
||||
|
||||
// Try to connect to a socket with the long name.
|
||||
do_check_throws_nsIException(() => socketTransportService.createUnixDomainTransport(socketName),
|
||||
"NS_ERROR_FILE_NAME_TOO_LONG");
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
// Going offline should not shut down Unix domain sockets.
|
||||
function test_keep_when_offline()
|
||||
{
|
||||
let log = '';
|
||||
|
||||
let socketName = do_get_tempdir();
|
||||
socketName.append('keep-when-offline');
|
||||
|
||||
// Create a listening socket.
|
||||
let listener = new UnixServerSocket(socketName, allPermissions, -1);
|
||||
listener.asyncListen({ onSocketAccepted: onAccepted, onStopListening: onStopListening });
|
||||
|
||||
// Connect a client socket to the listening socket.
|
||||
let client = socketTransportService.createUnixDomainTransport(socketName);
|
||||
let clientOutput = client.openOutputStream(0, 0, 0);
|
||||
let clientInput = client.openInputStream(0, 0, 0);
|
||||
clientInput.asyncWait(clientReady, 0, 0, threadManager.currentThread);
|
||||
let clientScriptableInput = new ScriptableInputStream(clientInput);
|
||||
|
||||
let server, serverInput, serverScriptableInput, serverOutput;
|
||||
|
||||
// How many times has the server invited the client to go first?
|
||||
let count = 0;
|
||||
|
||||
// The server accepted connection callback.
|
||||
function onAccepted(aListener, aServer) {
|
||||
do_print("test_keep_when_offline: onAccepted called");
|
||||
log += 'a';
|
||||
do_check_eq(aListener, listener);
|
||||
server = aServer;
|
||||
|
||||
// Prepare to receive messages from the client.
|
||||
serverInput = server.openInputStream(0, 0, 0);
|
||||
serverInput.asyncWait(serverReady, 0, 0, threadManager.currentThread);
|
||||
serverScriptableInput = new ScriptableInputStream(serverInput);
|
||||
|
||||
// Start a conversation with the client.
|
||||
serverOutput = server.openOutputStream(0, 0, 0);
|
||||
serverOutput.write("After you, Alphonse!", 20);
|
||||
count++;
|
||||
}
|
||||
|
||||
// The client has seen its end of the socket close.
|
||||
function clientReady(aStream) {
|
||||
log += 'c';
|
||||
do_print("test_keep_when_offline: clientReady called: " + log);
|
||||
do_check_eq(aStream, clientInput);
|
||||
|
||||
// If the connection has been closed, end the conversation and stop listening.
|
||||
let available;
|
||||
try {
|
||||
available = clientInput.available();
|
||||
} catch (ex) {
|
||||
do_check_instanceof(ex, Ci.nsIException);
|
||||
do_check_eq(ex.result, Cr.NS_BASE_STREAM_CLOSED);
|
||||
|
||||
do_print("client received end-of-stream; closing client output stream");
|
||||
log += ')';
|
||||
|
||||
client.close(Cr.NS_OK);
|
||||
|
||||
// Now both output streams have been closed, and both input streams
|
||||
// have received the close notification. Stop listening for
|
||||
// connections.
|
||||
listener.close();
|
||||
}
|
||||
|
||||
if (available) {
|
||||
// Check the message from the server.
|
||||
do_check_eq(clientScriptableInput.readBytes(20), "After you, Alphonse!");
|
||||
|
||||
// Write our response to the server.
|
||||
clientOutput.write("No, after you, Gaston!", 22);
|
||||
|
||||
// Ask to be called again, when more input arrives.
|
||||
clientInput.asyncWait(clientReady, 0, 0, threadManager.currentThread);
|
||||
}
|
||||
}
|
||||
|
||||
function serverReady(aStream) {
|
||||
log += 's';
|
||||
do_print("test_keep_when_offline: serverReady called: " + log);
|
||||
do_check_eq(aStream, serverInput);
|
||||
|
||||
// Check the message from the client.
|
||||
do_check_eq(serverScriptableInput.readBytes(22), "No, after you, Gaston!");
|
||||
|
||||
// This should not shut things down: Unix domain sockets should
|
||||
// remain open in offline mode.
|
||||
if (count == 5) {
|
||||
IOService.offline = true;
|
||||
log += 'o';
|
||||
}
|
||||
|
||||
if (count < 10) {
|
||||
// Insist.
|
||||
serverOutput.write("After you, Alphonse!", 20);
|
||||
count++;
|
||||
|
||||
// As long as the input stream is open, always ask to be called again
|
||||
// when more input arrives.
|
||||
serverInput.asyncWait(serverReady, 0, 0, threadManager.currentThread);
|
||||
} else if (count == 10) {
|
||||
// After sending ten times and receiving ten replies, we're not
|
||||
// going to send any more. Close the server's output stream; the
|
||||
// client's input stream should see this.
|
||||
do_print("closing server transport");
|
||||
server.close(Cr.NS_OK);
|
||||
log += '(';
|
||||
}
|
||||
}
|
||||
|
||||
// We have stopped listening.
|
||||
function onStopListening(aServ, aStatus) {
|
||||
do_print("test_keep_when_offline: onStopListening called");
|
||||
log += 'L';
|
||||
do_check_eq(log, 'acscscscscsocscscscscs(c)L');
|
||||
|
||||
do_check_eq(aServ, listener);
|
||||
do_check_eq(aStatus, Cr.NS_BINDING_ABORTED);
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
}
|
@ -2,6 +2,11 @@
|
||||
head = head_channels.js head_cache.js
|
||||
tail =
|
||||
|
||||
[test_unix_domain.js]
|
||||
# The xpcshell temp directory on Android doesn't seem to let us create
|
||||
# Unix domain sockets. (Perhaps it's a FAT filesystem?)
|
||||
skip-if = os == "android"
|
||||
|
||||
[test_addr_in_use_error.js]
|
||||
[test_304_responses.js]
|
||||
# Bug 675039: test hangs on Android-armv6
|
||||
|
@ -925,6 +925,18 @@ function legible_exception(exception)
|
||||
}
|
||||
}
|
||||
|
||||
function do_check_instanceof(value, constructor,
|
||||
stack=Components.stack.caller, todo=false) {
|
||||
do_report_result(value instanceof constructor,
|
||||
"value should be an instance of " + constructor.name,
|
||||
stack, todo);
|
||||
}
|
||||
|
||||
function todo_check_instanceof(value, constructor,
|
||||
stack=Components.stack.caller) {
|
||||
do_check_instanceof(value, constructor, stack, true);
|
||||
}
|
||||
|
||||
function do_test_pending(aName) {
|
||||
++_tests_pending;
|
||||
|
||||
|
@ -18,9 +18,21 @@
|
||||
interface nsISimpleEnumerator;
|
||||
|
||||
/**
|
||||
* This is the only correct cross-platform way to specify a file.
|
||||
* Strings are not such a way. If you grew up on windows or unix, you
|
||||
* may think they are. Welcome to reality.
|
||||
* An nsIFile is an abstract representation of a filename. It manages
|
||||
* filename encoding issues, pathname component separators ('/' vs. '\\'
|
||||
* vs. ':') and weird stuff like differing volumes with identical names, as
|
||||
* on pre-Darwin Macintoshes.
|
||||
*
|
||||
* This file has long introduced itself to new hackers with this opening
|
||||
* paragraph:
|
||||
*
|
||||
* This is the only correct cross-platform way to specify a file.
|
||||
* Strings are not such a way. If you grew up on windows or unix, you
|
||||
* may think they are. Welcome to reality.
|
||||
*
|
||||
* While taking the pose struck here to heart would be uncalled for, one
|
||||
* may safely conclude that writing cross-platform code is an embittering
|
||||
* experience.
|
||||
*
|
||||
* All methods with string parameters have two forms. The preferred
|
||||
* form operates on UCS-2 encoded characters strings. An alternate
|
||||
|
Loading…
Reference in New Issue
Block a user