blob: 6b04f3d79713ab90747d26b247d1c71789690bb4 [file] [log] [blame]
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
* vim: set ts=8 sts=4 et sw=4 tw=99:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// OSObject.h - os object for exposing posix system calls in the JS shell
#include "shell/OSObject.h"
#include <errno.h>
#include <stdlib.h>
#if defined(STARBOARD)
// Intentionally empty.
#elif defined(XP_WIN)
#include <direct.h>
#include <process.h>
#include <string.h>
#else
#include <sys/wait.h>
#include <unistd.h>
#endif
#include "jsapi.h"
// For JSFunctionSpecWithHelp
#include "jsfriendapi.h"
#include "jsobj.h"
#ifdef XP_WIN
# include "jswin.h"
#endif
#include "jswrapper.h"
#include "js/Conversions.h"
#include "shell/jsshell.h"
#include "vm/StringBuffer.h"
#include "vm/TypedArrayObject.h"
#include "jsobjinlines.h"
#ifdef XP_WIN
# define PATH_MAX (MAX_PATH > _MAX_DIR ? MAX_PATH : _MAX_DIR)
# define getcwd _getcwd
#else
# include <libgen.h>
#endif
using namespace JS;
namespace js {
namespace shell {
#ifdef XP_WIN
const char PathSeparator = '\\';
#else
const char PathSeparator = '/';
#endif
static bool
IsAbsolutePath(const JSAutoByteString& filename)
{
const char* pathname = filename.ptr();
if (pathname[0] == PathSeparator)
return true;
#ifdef XP_WIN
// On Windows there are various forms of absolute paths (see
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
// for details):
//
// "\..."
// "\\..."
// "C:\..."
//
// The first two cases are handled by the test above so we only need a test
// for the last one here.
if ((strlen(pathname) > 3 &&
isalpha(pathname[0]) && pathname[1] == ':' && pathname[2] == '\\'))
{
return true;
}
#endif
return false;
}
/*
* Resolve a (possibly) relative filename to an absolute path. If
* |scriptRelative| is true, then the result will be relative to the directory
* containing the currently-running script, or the current working directory if
* the currently-running script is "-e" (namely, you're using it from the
* command line.) Otherwise, it will be relative to the current working
* directory.
*/
JSString*
ResolvePath(JSContext* cx, HandleString filenameStr, PathResolutionMode resolveMode)
{
#if defined(STARBOARD)
return nullptr;
#else // defined(STARBOARD)
JSAutoByteString filename(cx, filenameStr);
if (!filename)
return nullptr;
if (IsAbsolutePath(filename))
return filenameStr;
/* Get the currently executing script's name. */
JS::AutoFilename scriptFilename;
if (!DescribeScriptedCaller(cx, &scriptFilename))
return nullptr;
if (!scriptFilename.get())
return nullptr;
if (strcmp(scriptFilename.get(), "-e") == 0 || strcmp(scriptFilename.get(), "typein") == 0)
resolveMode = RootRelative;
static char buffer[PATH_MAX+1];
if (resolveMode == ScriptRelative) {
#ifdef XP_WIN
// The docs say it can return EINVAL, but the compiler says it's void
_splitpath(scriptFilename.get(), nullptr, buffer, nullptr, nullptr);
#else
strncpy(buffer, scriptFilename.get(), PATH_MAX+1);
if (buffer[PATH_MAX] != '\0')
return nullptr;
// dirname(buffer) might return buffer, or it might return a
// statically-allocated string
memmove(buffer, dirname(buffer), strlen(buffer) + 1);
#endif
} else {
const char* cwd = getcwd(buffer, PATH_MAX);
if (!cwd)
return nullptr;
}
size_t len = strlen(buffer);
buffer[len] = '/';
strncpy(buffer + len + 1, filename.ptr(), sizeof(buffer) - (len+1));
if (buffer[PATH_MAX] != '\0')
return nullptr;
return JS_NewStringCopyZ(cx, buffer);
#endif // defined(STARBOARD)
}
static JSObject*
FileAsTypedArray(JSContext* cx, const char* pathname)
{
FILE* file = fopen(pathname, "rb");
if (!file) {
JS_ReportError(cx, "can't open %s: %s", pathname, strerror(errno));
return nullptr;
}
AutoCloseFile autoClose(file);
RootedObject obj(cx);
if (fseek(file, 0, SEEK_END) != 0) {
JS_ReportError(cx, "can't seek end of %s", pathname);
} else {
size_t len = ftell(file);
if (fseek(file, 0, SEEK_SET) != 0) {
JS_ReportError(cx, "can't seek start of %s", pathname);
} else {
obj = JS_NewUint8Array(cx, len);
if (!obj)
return nullptr;
js::TypedArrayObject& ta = obj->as<js::TypedArrayObject>();
if (ta.isSharedMemory()) {
// Must opt in to use shared memory. For now, don't.
//
// (It is incorrect to read into the buffer without
// synchronization since that can create a race. A
// lock here won't fix it - both sides must
// participate. So what one must do is to create a
// temporary buffer, read into that, and use a
// race-safe primitive to copy memory into the
// buffer.)
JS_ReportError(cx, "can't read %s: shared memory buffer", pathname);
return nullptr;
}
char* buf = static_cast<char*>(ta.viewDataUnshared());
size_t cc = fread(buf, 1, len, file);
if (cc != len) {
JS_ReportError(cx, "can't read %s: %s", pathname,
(ptrdiff_t(cc) < 0) ? strerror(errno) : "short read");
obj = nullptr;
}
}
}
return obj;
}
static bool
ReadFile(JSContext* cx, unsigned argc, Value* vp, bool scriptRelative)
{
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() < 1 || args.length() > 2) {
JS_ReportErrorNumber(cx, js::shell::my_GetErrorMessage, nullptr,
args.length() < 1 ? JSSMSG_NOT_ENOUGH_ARGS : JSSMSG_TOO_MANY_ARGS,
"snarf");
return false;
}
if (!args[0].isString() || (args.length() == 2 && !args[1].isString())) {
JS_ReportErrorNumber(cx, js::shell::my_GetErrorMessage, nullptr, JSSMSG_INVALID_ARGS, "snarf");
return false;
}
RootedString givenPath(cx, args[0].toString());
RootedString str(cx, js::shell::ResolvePath(cx, givenPath, scriptRelative ? ScriptRelative : RootRelative));
if (!str)
return false;
JSAutoByteString filename(cx, str);
if (!filename)
return false;
if (args.length() > 1) {
JSString* opt = JS::ToString(cx, args[1]);
if (!opt)
return false;
bool match;
if (!JS_StringEqualsAscii(cx, opt, "binary", &match))
return false;
if (match) {
JSObject* obj;
if (!(obj = FileAsTypedArray(cx, filename.ptr())))
return false;
args.rval().setObject(*obj);
return true;
}
}
if (!(str = FileAsString(cx, filename.ptr())))
return false;
args.rval().setString(str);
return true;
}
static bool
osfile_readFile(JSContext* cx, unsigned argc, Value* vp)
{
return ReadFile(cx, argc, vp, false);
}
static bool
osfile_readRelativeToScript(JSContext* cx, unsigned argc, Value* vp)
{
return ReadFile(cx, argc, vp, true);
}
static bool
osfile_writeTypedArrayToFile(JSContext* cx, unsigned argc, Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() != 2 ||
!args[0].isString() ||
!args[1].isObject() ||
!args[1].toObject().is<TypedArrayObject>())
{
JS_ReportErrorNumber(cx, my_GetErrorMessage, nullptr,
JSSMSG_INVALID_ARGS, "writeTypedArrayToFile");
return false;
}
RootedString givenPath(cx, args[0].toString());
RootedString str(cx, ResolvePath(cx, givenPath, RootRelative));
if (!str)
return false;
JSAutoByteString filename(cx, str);
if (!filename)
return false;
FILE* file = fopen(filename.ptr(), "wb");
if (!file) {
JS_ReportError(cx, "can't open %s: %s", filename.ptr(), strerror(errno));
return false;
}
AutoCloseFile autoClose(file);
TypedArrayObject* obj = &args[1].toObject().as<TypedArrayObject>();
if (obj->isSharedMemory()) {
// Must opt in to use shared memory. For now, don't.
//
// See further comments in FileAsTypedArray, above.
JS_ReportError(cx, "can't write %s: shared memory buffer", filename.ptr());
return false;
}
void* buf = obj->viewDataUnshared();
if (fwrite(buf, obj->bytesPerElement(), obj->length(), file) != obj->length() ||
!autoClose.release())
{
JS_ReportError(cx, "can't write %s", filename.ptr());
return false;
}
args.rval().setUndefined();
return true;
}
static bool
Redirect(JSContext* cx, FILE* fp, HandleString relFilename)
{
RootedString filename(cx, ResolvePath(cx, relFilename, RootRelative));
if (!filename)
return false;
JSAutoByteString filenameABS(cx, filename);
if (!filenameABS)
return false;
if (freopen(filenameABS.ptr(), "wb", fp) == nullptr) {
JS_ReportError(cx, "cannot redirect to %s: %s", filenameABS.ptr(), strerror(errno));
return false;
}
return true;
}
static bool
osfile_redirect(JSContext* cx, unsigned argc, Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() < 1 || args.length() > 2) {
JS_ReportErrorNumber(cx, my_GetErrorMessage, nullptr, JSSMSG_INVALID_ARGS, "redirect");
return false;
}
if (args[0].isString()) {
RootedString stdoutPath(cx, args[0].toString());
if (!stdoutPath)
return false;
if (!Redirect(cx, stdout, stdoutPath))
return false;
}
if (args.length() > 1 && args[1].isString()) {
RootedString stderrPath(cx, args[1].toString());
if (!stderrPath)
return false;
if (!Redirect(cx, stderr, stderrPath))
return false;
}
args.rval().setUndefined();
return true;
}
static const JSFunctionSpecWithHelp osfile_functions[] = {
JS_FN_HELP("readFile", osfile_readFile, 1, 0,
"readFile(filename, [\"binary\"])",
" Read filename into returned string. Filename is relative to the current\n"
" working directory."),
JS_FN_HELP("readRelativeToScript", osfile_readRelativeToScript, 1, 0,
"readRelativeToScript(filename, [\"binary\"])",
" Read filename into returned string. Filename is relative to the directory\n"
" containing the current script."),
JS_FS_HELP_END
};
static const JSFunctionSpecWithHelp osfile_unsafe_functions[] = {
JS_FN_HELP("writeTypedArrayToFile", osfile_writeTypedArrayToFile, 2, 0,
"writeTypedArrayToFile(filename, data)",
" Write the contents of a typed array to the named file."),
JS_FN_HELP("redirect", osfile_redirect, 2, 0,
"redirect(stdoutFilename[, stderrFilename])",
" Redirect stdout and/or stderr to the named file. Pass undefined to avoid\n"
" redirecting. Filenames are relative to the current working directory."),
JS_FS_HELP_END
};
static bool
ospath_isAbsolute(JSContext* cx, unsigned argc, Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() != 1 || !args[0].isString()) {
JS_ReportErrorNumber(cx, my_GetErrorMessage, nullptr, JSSMSG_INVALID_ARGS, "isAbsolute");
return false;
}
JSAutoByteString path(cx, args[0].toString());
if (!path)
return false;
args.rval().setBoolean(IsAbsolutePath(path));
return true;
}
static bool
ospath_join(JSContext* cx, unsigned argc, Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() < 1) {
JS_ReportErrorNumber(cx, my_GetErrorMessage, nullptr, JSSMSG_INVALID_ARGS, "join");
return false;
}
// This function doesn't take into account some aspects of Windows paths,
// e.g. the drive letter is always reset when an absolute path is appended.
StringBuffer buffer(cx);
for (unsigned i = 0; i < args.length(); i++) {
if (!args[i].isString()) {
JS_ReportError(cx, "join expects string arguments only");
return false;
}
JSAutoByteString path(cx, args[i].toString());
if (!path)
return false;
if (IsAbsolutePath(path)) {
MOZ_ALWAYS_TRUE(buffer.resize(0));
} else if (i != 0) {
if (!buffer.append(PathSeparator))
return false;
}
if (!buffer.append(args[i].toString()))
return false;
}
JSString* result = buffer.finishString();
if (!result)
return false;
args.rval().setString(result);
return true;
}
static const JSFunctionSpecWithHelp ospath_functions[] = {
JS_FN_HELP("isAbsolute", ospath_isAbsolute, 1, 0,
"isAbsolute(path)",
" Return whether the given path is absolute."),
JS_FN_HELP("join", ospath_join, 1, 0,
"join(paths...)",
" Join one or more path components in a platform independent way."),
JS_FS_HELP_END
};
static bool
os_getenv(JSContext* cx, unsigned argc, Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() < 1) {
JS_ReportError(cx, "os.getenv requires 1 argument");
return false;
}
RootedString key(cx, ToString(cx, args[0]));
if (!key)
return false;
JSAutoByteString keyBytes;
if (!keyBytes.encodeUtf8(cx, key))
return false;
if (const char* valueBytes = js_sb_getenv(keyBytes.ptr())) {
RootedString value(cx, JS_NewStringCopyZ(cx, valueBytes));
if (!value)
return false;
args.rval().setString(value);
} else {
args.rval().setUndefined();
}
return true;
}
static bool
os_getpid(JSContext* cx, unsigned argc, Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() != 0) {
JS_ReportError(cx, "os.getpid takes no arguments");
return false;
}
#if defined(STARBOARD)
args.rval().setInt32(0);
#else
args.rval().setInt32(getpid());
#endif
return true;
}
#if !defined(XP_WIN)
// There are two possible definitions of strerror_r floating around. The GNU
// one returns a char* which may or may not be the buffer you passed in. The
// other one returns an integer status code, and always writes the result into
// the provided buffer.
inline char*
strerror_message(int result, char* buffer)
{
return result == 0 ? buffer : nullptr;
}
inline char*
strerror_message(char* result, char* buffer)
{
return result;
}
#endif
static void
ReportSysError(JSContext* cx, const char* prefix)
{
char buffer[200];
#if defined(XP_WIN)
strerror_s(buffer, sizeof(buffer), errno);
const char* errstr = buffer;
#else
const char* errstr = strerror_message(strerror_r(errno, buffer, sizeof(buffer)), buffer);
#endif
if (!errstr)
errstr = "unknown error";
size_t nbytes = strlen(prefix) + strlen(errstr) + 3;
char* final = (char*) js_malloc(nbytes);
if (!final) {
JS_ReportOutOfMemory(cx);
return;
}
#ifdef XP_WIN
_snprintf(final, nbytes, "%s: %s", prefix, errstr);
#else
snprintf(final, nbytes, "%s: %s", prefix, errstr);
#endif
JS_ReportError(cx, final);
js_free(final);
}
static bool
os_system(JSContext* cx, unsigned argc, Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() == 0) {
JS_ReportError(cx, "os.system requires 1 argument");
return false;
}
JSString* str = JS::ToString(cx, args[0]);
if (!str)
return false;
JSAutoByteString command(cx, str);
if (!command)
return false;
int result = system(command.ptr());
if (result == -1) {
ReportSysError(cx, "system call failed");
return false;
}
args.rval().setInt32(result);
return true;
}
#if !defined(XP_WIN) && !defined(STARBOARD)
static bool
os_spawn(JSContext* cx, unsigned argc, Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
if (args.length() == 0) {
JS_ReportError(cx, "os.spawn requires 1 argument");
return false;
}
JSString* str = JS::ToString(cx, args[0]);
if (!str)
return false;
JSAutoByteString command(cx, str);
if (!command)
return false;
int32_t childPid = fork();
if (childPid == -1) {
ReportSysError(cx, "fork failed");
return false;
}
if (childPid) {
args.rval().setInt32(childPid);
return true;
}
// We are in the child
const char* cmd[] = {"sh", "-c", nullptr, nullptr};
cmd[2] = command.ptr();
execvp("sh", (char * const*)cmd);
exit(1);
}
static bool
os_kill(JSContext* cx, unsigned argc, Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
int32_t pid;
if (args.length() < 1) {
JS_ReportError(cx, "os.kill requires 1 argument");
return false;
}
if (!JS::ToInt32(cx, args[0], &pid))
return false;
// It is too easy to kill yourself accidentally with os.kill("goose").
if (pid == 0 && !args[0].isInt32()) {
JS_ReportError(cx, "os.kill requires numeric pid");
return false;
}
int signal = SIGINT;
if (args.length() > 1) {
if (!JS::ToInt32(cx, args[1], &signal))
return false;
}
int status = kill(pid, signal);
if (status == -1) {
ReportSysError(cx, "kill failed");
return false;
}
args.rval().setUndefined();
return true;
}
static bool
os_waitpid(JSContext* cx, unsigned argc, Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
int32_t pid;
if (args.length() == 0) {
pid = -1;
} else {
if (!JS::ToInt32(cx, args[0], &pid))
return false;
}
bool nohang = false;
if (args.length() >= 2)
nohang = JS::ToBoolean(args[1]);
int status = 0;
pid_t result = waitpid(pid, &status, nohang ? WNOHANG : 0);
if (result == -1) {
ReportSysError(cx, "os.waitpid failed");
return false;
}
RootedObject info(cx, JS_NewPlainObject(cx));
if (!info)
return false;
RootedValue v(cx);
if (result != 0) {
v.setInt32(result);
if (!JS_DefineProperty(cx, info, "pid", v, JSPROP_ENUMERATE))
return false;
if (WIFEXITED(status)) {
v.setInt32(WEXITSTATUS(status));
if (!JS_DefineProperty(cx, info, "exitStatus", v, JSPROP_ENUMERATE))
return false;
}
}
args.rval().setObject(*info);
return true;
}
#endif
static const JSFunctionSpecWithHelp os_functions[] = {
JS_FN_HELP("getenv", os_getenv, 1, 0,
"getenv(variable)",
" Get the value of an environment variable."),
JS_FN_HELP("getpid", os_getpid, 0, 0,
"getpid()",
" Return the current process id."),
JS_FN_HELP("system", os_system, 1, 0,
"system(command)",
" Execute command on the current host, returning result code or throwing an\n"
" exception on failure."),
#if !defined(XP_WIN) && !defined(STARBOARD)
JS_FN_HELP("spawn", os_spawn, 1, 0,
"spawn(command)",
" Start up a separate process running the given command. Returns the pid."),
JS_FN_HELP("kill", os_kill, 1, 0,
"kill(pid[, signal])",
" Send a signal to the given pid. The default signal is SIGINT. The signal\n"
" passed in must be numeric, if given."),
JS_FN_HELP("waitpid", os_waitpid, 1, 0,
"waitpid(pid[, nohang])",
" Calls waitpid(). 'nohang' is a boolean indicating whether to pass WNOHANG.\n"
" The return value is an object containing a 'pid' field, if a process was waitable\n"
" and an 'exitStatus' field if a pid exited."),
#endif
JS_FS_HELP_END
};
bool
DefineOS(JSContext* cx, HandleObject global, bool fuzzingSafe)
{
RootedObject obj(cx, JS_NewPlainObject(cx));
if (!obj || !JS_DefineProperty(cx, global, "os", obj, 0))
return false;
if (!fuzzingSafe) {
if (!JS_DefineFunctionsWithHelp(cx, obj, os_functions))
return false;
}
RootedObject osfile(cx, JS_NewPlainObject(cx));
if (!osfile ||
!JS_DefineFunctionsWithHelp(cx, osfile, osfile_functions) ||
!JS_DefineProperty(cx, obj, "file", osfile, 0))
{
return false;
}
if (!fuzzingSafe) {
if (!JS_DefineFunctionsWithHelp(cx, osfile, osfile_unsafe_functions))
return false;
}
RootedObject ospath(cx, JS_NewPlainObject(cx));
if (!ospath ||
!JS_DefineFunctionsWithHelp(cx, ospath, ospath_functions) ||
!JS_DefineProperty(cx, obj, "path", ospath, 0))
{
return false;
}
// For backwards compatibility, expose various os.file.* functions as
// direct methods on the global.
RootedValue val(cx);
struct {
const char* src;
const char* dst;
} osfile_exports[] = {
{ "readFile", "read" },
{ "readFile", "snarf" },
{ "readRelativeToScript", "readRelativeToScript" },
{ "redirect", "redirect" }
};
for (auto pair : osfile_exports) {
if (!JS_GetProperty(cx, osfile, pair.src, &val))
return false;
if (val.isObject()) {
RootedObject function(cx, &val.toObject());
if (!JS_DefineProperty(cx, global, pair.dst, function, 0))
return false;
}
}
return true;
}
} // namespace shell
} // namespace js