summaryrefslogtreecommitdiffstats
path: root/js/src/shell/OSObject.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /js/src/shell/OSObject.cpp
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/shell/OSObject.cpp')
-rw-r--r--js/src/shell/OSObject.cpp1309
1 files changed, 1309 insertions, 0 deletions
diff --git a/js/src/shell/OSObject.cpp b/js/src/shell/OSObject.cpp
new file mode 100644
index 0000000000..b618d331e0
--- /dev/null
+++ b/js/src/shell/OSObject.cpp
@@ -0,0 +1,1309 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=8 sts=2 et sw=2 tw=80:
+ * 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 "mozilla/ArrayUtils.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/TextUtils.h"
+
+#include <errno.h>
+#include <stdlib.h>
+#ifdef XP_WIN
+# include <direct.h>
+# include <process.h>
+# include <string.h>
+# include <wchar.h>
+# include <windows.h>
+#elif __wasi__
+# include <dirent.h>
+# include <sys/types.h>
+# include <unistd.h>
+#else
+# include <dirent.h>
+# include <sys/types.h>
+# include <sys/wait.h>
+# include <unistd.h>
+#endif
+
+#include "jsapi.h"
+// For JSFunctionSpecWithHelp
+#include "jsfriendapi.h"
+
+#include "gc/GCContext.h"
+#include "js/CharacterEncoding.h"
+#include "js/Conversions.h"
+#include "js/experimental/TypedData.h" // JS_NewUint8Array
+#include "js/Object.h" // JS::GetReservedSlot
+#include "js/PropertyAndElement.h" // JS_DefineProperty
+#include "js/PropertySpec.h"
+#include "js/Value.h" // JS::Value
+#include "js/Wrapper.h"
+#include "shell/jsshell.h"
+#include "shell/StringUtils.h"
+#include "util/GetPidProvider.h" // getpid()
+#include "util/StringBuffer.h"
+#include "util/Text.h"
+#include "util/WindowsWrapper.h"
+#include "vm/JSObject.h"
+#include "vm/TypedArrayObject.h"
+
+#include "vm/JSObject-inl.h"
+
+#ifdef XP_WIN
+# ifndef PATH_MAX
+# define PATH_MAX (MAX_PATH > _MAX_DIR ? MAX_PATH : _MAX_DIR)
+# endif
+# define getcwd _getcwd
+#elif defined(__wasi__)
+// Nothing.
+#else
+# include <libgen.h>
+#endif
+
+using js::shell::RCFile;
+
+namespace js {
+namespace shell {
+
+bool IsAbsolutePath(JSLinearString* filename) {
+ size_t length = filename->length();
+
+#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 common test below so we only need a
+ // specific test for the last one here.
+
+ if (length > 3 && mozilla::IsAsciiAlpha(CharAt(filename, 0)) &&
+ CharAt(filename, 1) == u':' && CharAt(filename, 2) == u'\\') {
+ return true;
+ }
+#endif
+
+ return length > 0 && CharAt(filename, 0) == PathSeparator;
+}
+
+static UniqueChars DirectoryName(JSContext* cx, const char* path) {
+#ifdef XP_WIN
+ UniqueWideChars widePath = JS::EncodeUtf8ToWide(cx, path);
+ if (!widePath) {
+ return nullptr;
+ }
+
+ wchar_t dirName[PATH_MAX + 1];
+ wchar_t* drive = nullptr;
+ wchar_t* fileName = nullptr;
+ wchar_t* fileExt = nullptr;
+
+ // The docs say it can return EINVAL, but the compiler says it's void
+ _wsplitpath(widePath.get(), drive, dirName, fileName, fileExt);
+
+ return JS::EncodeWideToUtf8(cx, dirName);
+#else
+ UniqueChars narrowPath = JS::EncodeUtf8ToNarrow(cx, path);
+ if (!narrowPath) {
+ return nullptr;
+ }
+
+ char dirName[PATH_MAX + 1];
+ strncpy(dirName, narrowPath.get(), PATH_MAX);
+ if (dirName[PATH_MAX - 1] != '\0') {
+ return nullptr;
+ }
+
+# ifdef __wasi__
+ // dirname() seems not to behave properly with wasi-libc; so we do our own
+ // simple thing here.
+ char* p = dirName + strlen(dirName);
+ bool found = false;
+ while (p > dirName) {
+ if (*p == '/') {
+ found = true;
+ *p = '\0';
+ break;
+ }
+ p--;
+ }
+ if (!found) {
+ // There's no '/'. Possible cases are the following:
+ // * "."
+ // * ".."
+ // * filename only
+ //
+ // dirname() returns "." for all cases.
+ dirName[0] = '.';
+ dirName[1] = '\0';
+ }
+# else
+ // dirname(dirName) might return dirName, or it might return a
+ // statically-allocated string
+ memmove(dirName, dirname(dirName), strlen(dirName) + 1);
+# endif
+
+ return JS::EncodeNarrowToUtf8(cx, dirName);
+#endif
+}
+
+/*
+ * 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 (!filenameStr) {
+#ifdef XP_WIN
+ return JS_NewStringCopyZ(cx, "nul");
+#elif defined(__wasi__)
+ MOZ_CRASH("NYI for WASI");
+ return nullptr;
+#else
+ return JS_NewStringCopyZ(cx, "/dev/null");
+#endif
+ }
+
+ Rooted<JSLinearString*> str(cx, JS_EnsureLinearString(cx, filenameStr));
+ if (!str) {
+ return nullptr;
+ }
+
+ if (IsAbsolutePath(str)) {
+ return str;
+ }
+
+ UniqueChars filename = JS_EncodeStringToUTF8(cx, str);
+ if (!filename) {
+ return nullptr;
+ }
+
+ JS::AutoFilename scriptFilename;
+ if (resolveMode == ScriptRelative) {
+ // Get the currently executing script's name.
+ if (!DescribeScriptedCaller(cx, &scriptFilename)) {
+ return nullptr;
+ }
+
+ if (!scriptFilename.get()) {
+ return nullptr;
+ }
+
+ if (strcmp(scriptFilename.get(), "-e") == 0 ||
+ strcmp(scriptFilename.get(), "typein") == 0) {
+ resolveMode = RootRelative;
+ }
+ }
+
+ UniqueChars path;
+ if (resolveMode == ScriptRelative) {
+ path = DirectoryName(cx, scriptFilename.get());
+ } else {
+ path = GetCWD(cx);
+ }
+
+ if (!path) {
+ return nullptr;
+ }
+
+ size_t pathLen = strlen(path.get());
+ size_t filenameLen = strlen(filename.get());
+ size_t resultLen = pathLen + 1 + filenameLen;
+
+ UniqueChars result = cx->make_pod_array<char>(resultLen + 1);
+ if (!result) {
+ return nullptr;
+ }
+ memcpy(result.get(), path.get(), pathLen);
+ result[pathLen] = '/';
+ memcpy(result.get() + pathLen + 1, filename.get(), filenameLen);
+ result[pathLen + 1 + filenameLen] = '\0';
+
+ return JS_NewStringCopyUTF8N(cx, JS::UTF8Chars(result.get(), resultLen));
+}
+
+FILE* OpenFile(JSContext* cx, const char* filename, const char* mode) {
+#ifdef XP_WIN
+ // Maximum valid mode string is "w+xb". Longer strings or strings
+ // containing invalid input lead to undefined behavior.
+ constexpr size_t MaxValidModeLength = 4;
+ wchar_t wideMode[MaxValidModeLength + 1] = {0};
+ for (size_t i = 0; i < MaxValidModeLength && mode[i] != '\0'; i++) {
+ wideMode[i] = mode[i] & 0x7f;
+ }
+
+ UniqueWideChars wideFilename = JS::EncodeUtf8ToWide(cx, filename);
+ if (!wideFilename) {
+ return nullptr;
+ }
+
+ FILE* file = _wfopen(wideFilename.get(), wideMode);
+#else
+ UniqueChars narrowFilename = JS::EncodeUtf8ToNarrow(cx, filename);
+ if (!narrowFilename) {
+ return nullptr;
+ }
+
+ FILE* file = fopen(narrowFilename.get(), mode);
+#endif
+
+ if (!file) {
+ if (UniqueChars error = SystemErrorMessage(cx, errno)) {
+ JS_ReportErrorNumberUTF8(cx, my_GetErrorMessage, nullptr,
+ JSSMSG_CANT_OPEN, filename, error.get());
+ }
+ return nullptr;
+ }
+ return file;
+}
+
+bool ReadFile(JSContext* cx, const char* filename, FILE* file, char* buffer,
+ size_t length) {
+ size_t cc = fread(buffer, sizeof(char), length, file);
+ if (cc != length) {
+ if (ptrdiff_t(cc) < 0) {
+ if (UniqueChars error = SystemErrorMessage(cx, errno)) {
+ JS_ReportErrorNumberUTF8(cx, my_GetErrorMessage, nullptr,
+ JSSMSG_CANT_READ, filename, error.get());
+ }
+ } else {
+ JS_ReportErrorUTF8(cx, "can't read %s: short read", filename);
+ }
+ return false;
+ }
+ return true;
+}
+
+bool FileSize(JSContext* cx, const char* filename, FILE* file, size_t* size) {
+ if (fseek(file, 0, SEEK_END) != 0) {
+ JS_ReportErrorUTF8(cx, "can't seek end of %s", filename);
+ return false;
+ }
+
+ size_t len = ftell(file);
+ if (fseek(file, 0, SEEK_SET) != 0) {
+ JS_ReportErrorUTF8(cx, "can't seek start of %s", filename);
+ return false;
+ }
+
+ *size = len;
+ return true;
+}
+
+JSObject* FileAsTypedArray(JSContext* cx, JS::HandleString pathnameStr) {
+ UniqueChars pathname = JS_EncodeStringToUTF8(cx, pathnameStr);
+ if (!pathname) {
+ return nullptr;
+ }
+
+ FILE* file = OpenFile(cx, pathname.get(), "rb");
+ if (!file) {
+ return nullptr;
+ }
+ AutoCloseFile autoClose(file);
+
+ size_t len;
+ if (!FileSize(cx, pathname.get(), file, &len)) {
+ return nullptr;
+ }
+
+ if (len > ArrayBufferObject::MaxByteLength) {
+ JS_ReportErrorUTF8(cx, "file %s is too large for a Uint8Array",
+ pathname.get());
+ return nullptr;
+ }
+
+ JS::Rooted<JSObject*> obj(cx, 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_ReportErrorUTF8(cx, "can't read %s: shared memory buffer",
+ pathname.get());
+ return nullptr;
+ }
+
+ char* buf = static_cast<char*>(ta.dataPointerUnshared());
+ if (!ReadFile(cx, pathname.get(), file, buf, len)) {
+ return nullptr;
+ }
+
+ return obj;
+}
+
+/**
+ * Return the current working directory or |null| on failure.
+ */
+UniqueChars GetCWD(JSContext* cx) {
+#ifdef XP_WIN
+ wchar_t buffer[PATH_MAX + 1];
+ const wchar_t* cwd = _wgetcwd(buffer, PATH_MAX);
+ if (!cwd) {
+ return nullptr;
+ }
+ return JS::EncodeWideToUtf8(cx, buffer);
+#else
+ char buffer[PATH_MAX + 1];
+ const char* cwd = getcwd(buffer, PATH_MAX);
+ if (!cwd) {
+ return nullptr;
+ }
+ return JS::EncodeNarrowToUtf8(cx, buffer);
+#endif
+}
+
+static bool ReadFile(JSContext* cx, unsigned argc, Value* vp,
+ PathResolutionMode resolveMode) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ if (args.length() < 1 || args.length() > 2) {
+ JS_ReportErrorNumberASCII(
+ 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_ReportErrorNumberASCII(cx, js::shell::my_GetErrorMessage, nullptr,
+ JSSMSG_INVALID_ARGS, "snarf");
+ return false;
+ }
+
+ JS::Rooted<JSString*> givenPath(cx, args[0].toString());
+ JS::Rooted<JSString*> str(cx,
+ js::shell::ResolvePath(cx, givenPath, resolveMode));
+ if (!str) {
+ return false;
+ }
+
+ if (args.length() > 1) {
+ JSString* opt = JS::ToString(cx, args[1]);
+ if (!opt) {
+ return false;
+ }
+ bool match;
+ if (!JS_StringEqualsLiteral(cx, opt, "binary", &match)) {
+ return false;
+ }
+ if (match) {
+ JSObject* obj;
+ if (!(obj = FileAsTypedArray(cx, str))) {
+ return false;
+ }
+ args.rval().setObject(*obj);
+ return true;
+ }
+ }
+
+ if (!(str = FileAsString(cx, str))) {
+ return false;
+ }
+ args.rval().setString(str);
+ return true;
+}
+
+static bool osfile_readFile(JSContext* cx, unsigned argc, Value* vp) {
+ return ReadFile(cx, argc, vp, RootRelative);
+}
+
+static bool osfile_readRelativeToScript(JSContext* cx, unsigned argc,
+ Value* vp) {
+ return ReadFile(cx, argc, vp, ScriptRelative);
+}
+
+static bool ListDir(JSContext* cx, unsigned argc, Value* vp,
+ PathResolutionMode resolveMode) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ if (args.length() != 1) {
+ JS_ReportErrorASCII(cx, "os.file.listDir requires 1 argument");
+ return false;
+ }
+
+ if (!args[0].isString()) {
+ JS_ReportErrorNumberASCII(cx, js::shell::my_GetErrorMessage, nullptr,
+ JSSMSG_INVALID_ARGS, "os.file.listDir");
+ return false;
+ }
+
+ RootedString givenPath(cx, args[0].toString());
+ RootedString str(cx, ResolvePath(cx, givenPath, resolveMode));
+ if (!str) {
+ return false;
+ }
+
+ UniqueChars pathname = JS_EncodeStringToUTF8(cx, str);
+ if (!pathname) {
+ JS_ReportErrorASCII(cx, "os.file.listDir cannot convert path to Latin1");
+ return false;
+ }
+
+ RootedValueVector elems(cx);
+ auto append = [&](const char* name) -> bool {
+ if (!(str = JS_NewStringCopyZ(cx, name))) {
+ return false;
+ }
+ if (!elems.append(StringValue(str))) {
+ js::ReportOutOfMemory(cx);
+ return false;
+ }
+ return true;
+ };
+
+#if defined(XP_UNIX)
+ {
+ DIR* dir = opendir(pathname.get());
+ if (!dir) {
+ JS_ReportErrorASCII(cx, "os.file.listDir is unable to open: %s",
+ pathname.get());
+ return false;
+ }
+ auto close = mozilla::MakeScopeExit([&] {
+ if (closedir(dir) != 0) {
+ MOZ_CRASH("Could not close dir");
+ }
+ });
+
+ while (struct dirent* entry = readdir(dir)) {
+ if (!append(entry->d_name)) {
+ return false;
+ }
+ }
+ }
+#elif defined(XP_WIN)
+ {
+ const size_t pathlen = strlen(pathname.get());
+ Vector<char> pattern(cx);
+ if (!pattern.append(pathname.get(), pathlen) ||
+ !pattern.append(PathSeparator) || !pattern.append("*", 2)) {
+ js::ReportOutOfMemory(cx);
+ return false;
+ }
+
+ WIN32_FIND_DATAA FindFileData;
+ HANDLE hFind = FindFirstFileA(pattern.begin(), &FindFileData);
+ auto close = mozilla::MakeScopeExit([&] {
+ if (!FindClose(hFind)) {
+ MOZ_CRASH("Could not close Find");
+ }
+ });
+ for (bool found = (hFind != INVALID_HANDLE_VALUE); found;
+ found = FindNextFileA(hFind, &FindFileData)) {
+ if (!append(FindFileData.cFileName)) {
+ return false;
+ }
+ }
+ }
+#endif
+
+ JSObject* array = JS::NewArrayObject(cx, elems);
+ if (!array) {
+ return false;
+ }
+
+ args.rval().setObject(*array);
+ return true;
+}
+
+static bool osfile_listDir(JSContext* cx, unsigned argc, Value* vp) {
+ return ListDir(cx, argc, vp, RootRelative);
+}
+
+static bool osfile_listDirRelativeToScript(JSContext* cx, unsigned argc,
+ Value* vp) {
+ return ListDir(cx, argc, vp, ScriptRelative);
+}
+
+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_ReportErrorNumberASCII(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;
+ }
+
+ UniqueChars filename = JS_EncodeStringToUTF8(cx, str);
+ if (!filename) {
+ return false;
+ }
+
+ FILE* file = OpenFile(cx, filename.get(), "wb");
+ if (!file) {
+ 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_ReportErrorUTF8(cx, "can't write %s: shared memory buffer",
+ filename.get());
+ return false;
+ }
+ void* buf = obj->dataPointerUnshared();
+ size_t length = obj->length();
+ if (fwrite(buf, obj->bytesPerElement(), length, file) != length ||
+ !autoClose.release()) {
+ JS_ReportErrorUTF8(cx, "can't write %s", filename.get());
+ return false;
+ }
+
+ args.rval().setUndefined();
+ return true;
+}
+
+/* static */
+RCFile* RCFile::create(JSContext* cx, const char* filename, const char* mode) {
+ FILE* fp = OpenFile(cx, filename, mode);
+ if (!fp) {
+ return nullptr;
+ }
+
+ RCFile* file = cx->new_<RCFile>(fp);
+ if (!file) {
+ fclose(fp);
+ return nullptr;
+ }
+
+ return file;
+}
+
+void RCFile::close() {
+ if (fp) {
+ fclose(fp);
+ }
+ fp = nullptr;
+}
+
+bool RCFile::release() {
+ if (--numRefs) {
+ return false;
+ }
+ this->close();
+ return true;
+}
+
+class FileObject : public NativeObject {
+ enum : uint32_t { FILE_SLOT = 0, NUM_SLOTS };
+
+ public:
+ static const JSClass class_;
+
+ static FileObject* create(JSContext* cx, RCFile* file) {
+ FileObject* obj = js::NewBuiltinClassInstance<FileObject>(cx);
+ if (!obj) {
+ return nullptr;
+ }
+
+ InitReservedSlot(obj, FILE_SLOT, file, MemoryUse::FileObjectFile);
+ file->acquire();
+ return obj;
+ }
+
+ static void finalize(JS::GCContext* gcx, JSObject* obj) {
+ FileObject* fileObj = &obj->as<FileObject>();
+ RCFile* file = fileObj->rcFile();
+ gcx->removeCellMemory(obj, sizeof(*file), MemoryUse::FileObjectFile);
+ if (file->release()) {
+ gcx->deleteUntracked(file);
+ }
+ }
+
+ bool isOpen() {
+ RCFile* file = rcFile();
+ return file && file->isOpen();
+ }
+
+ void close() {
+ if (!isOpen()) {
+ return;
+ }
+ rcFile()->close();
+ }
+
+ RCFile* rcFile() {
+ return reinterpret_cast<RCFile*>(
+ JS::GetReservedSlot(this, FILE_SLOT).toPrivate());
+ }
+};
+
+static const JSClassOps FileObjectClassOps = {
+ nullptr, // addProperty
+ nullptr, // delProperty
+ nullptr, // enumerate
+ nullptr, // newEnumerate
+ nullptr, // resolve
+ nullptr, // mayResolve
+ FileObject::finalize, // finalize
+ nullptr, // call
+ nullptr, // construct
+ nullptr, // trace
+};
+
+const JSClass FileObject::class_ = {
+ "File",
+ JSCLASS_HAS_RESERVED_SLOTS(FileObject::NUM_SLOTS) |
+ JSCLASS_FOREGROUND_FINALIZE,
+ &FileObjectClassOps};
+
+static FileObject* redirect(JSContext* cx, HandleString relFilename,
+ RCFile** globalFile) {
+ RootedString filename(cx, ResolvePath(cx, relFilename, RootRelative));
+ if (!filename) {
+ return nullptr;
+ }
+ UniqueChars filenameABS = JS_EncodeStringToUTF8(cx, filename);
+ if (!filenameABS) {
+ return nullptr;
+ }
+ RCFile* file = RCFile::create(cx, filenameABS.get(), "wb");
+ if (!file) {
+ return nullptr;
+ }
+
+ // Grant the global gOutFile ownership of the new file, release ownership
+ // of its old file, and return a FileObject owning the old file.
+ file->acquire(); // Global owner of new file
+
+ FileObject* fileObj =
+ FileObject::create(cx, *globalFile); // Newly created owner of old file
+ if (!fileObj) {
+ file->release();
+ return nullptr;
+ }
+
+ (*globalFile)->release(); // Release (global) ownership of old file.
+ *globalFile = file;
+
+ return fileObj;
+}
+
+static bool Redirect(JSContext* cx, const CallArgs& args, RCFile** outFile) {
+ if (args.length() > 1) {
+ JS_ReportErrorNumberASCII(cx, js::shell::my_GetErrorMessage, nullptr,
+ JSSMSG_INVALID_ARGS, "redirect");
+ return false;
+ }
+
+ RCFile* oldFile = *outFile;
+ RootedObject oldFileObj(cx, FileObject::create(cx, oldFile));
+ if (!oldFileObj) {
+ return false;
+ }
+
+ if (args.get(0).isUndefined()) {
+ args.rval().setObject(*oldFileObj);
+ return true;
+ }
+
+ if (args[0].isObject()) {
+ Rooted<FileObject*> fileObj(cx,
+ args[0].toObject().maybeUnwrapIf<FileObject>());
+ if (!fileObj) {
+ JS_ReportErrorNumberASCII(cx, js::shell::my_GetErrorMessage, nullptr,
+ JSSMSG_INVALID_ARGS, "redirect");
+ return false;
+ }
+
+ // Passed in a FileObject. Create a FileObject for the previous
+ // global file, and set the global file to the passed-in one.
+ *outFile = fileObj->rcFile();
+ (*outFile)->acquire();
+ oldFile->release();
+
+ args.rval().setObject(*oldFileObj);
+ return true;
+ }
+
+ RootedString filename(cx);
+ if (!args[0].isNull()) {
+ filename = JS::ToString(cx, args[0]);
+ if (!filename) {
+ return false;
+ }
+ }
+
+ if (!redirect(cx, filename, outFile)) {
+ return false;
+ }
+
+ args.rval().setObject(*oldFileObj);
+ return true;
+}
+
+static bool osfile_redirectOutput(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ ShellContext* scx = GetShellContext(cx);
+ return Redirect(cx, args, scx->outFilePtr);
+}
+
+static bool osfile_redirectError(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ ShellContext* scx = GetShellContext(cx);
+ return Redirect(cx, args, scx->errFilePtr);
+}
+
+static bool osfile_close(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ Rooted<FileObject*> fileObj(cx);
+ if (args.get(0).isObject()) {
+ fileObj = args[0].toObject().maybeUnwrapIf<FileObject>();
+ }
+
+ if (!fileObj) {
+ JS_ReportErrorNumberASCII(cx, js::shell::my_GetErrorMessage, nullptr,
+ JSSMSG_INVALID_ARGS, "close");
+ return false;
+ }
+
+ fileObj->close();
+
+ args.rval().setUndefined();
+ return true;
+}
+
+// clang-format off
+static const JSFunctionSpecWithHelp osfile_functions[] = {
+ JS_FN_HELP("readFile", osfile_readFile, 1, 0,
+"readFile(filename, [\"binary\"])",
+" Read entire contents of filename. Returns a string, unless \"binary\" is passed\n"
+" as the second argument, in which case it returns a Uint8Array. Filename is\n"
+" relative to the current 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_FN_HELP("listDir", osfile_listDir, 1, 0,
+"listDir(dirname)",
+" Read entire contents of a directory. The \"dirname\" parameter is relate to the\n"
+" current working directory. Returns a list of filenames within the given directory.\n"
+" Note that \".\" and \"..\" are also listed."),
+
+ JS_FN_HELP("listDirRelativeToScript", osfile_listDirRelativeToScript, 1, 0,
+"listDirRelativeToScript(dirname)",
+" Same as \"listDir\" except that the \"dirname\" is relative to the directory\n"
+" containing the current script."),
+
+ JS_FS_HELP_END
+};
+// clang-format on
+
+// clang-format off
+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_redirectOutput, 1, 0,
+"redirect([path-or-object])",
+" Redirect print() output to the named file.\n"
+" Return an opaque object representing the previous destination, which\n"
+" may be passed into redirect() later to restore the output."),
+
+ JS_FN_HELP("redirectErr", osfile_redirectError, 1, 0,
+"redirectErr([path-or-object])",
+" Same as redirect(), but for printErr"),
+
+ JS_FN_HELP("close", osfile_close, 1, 0,
+"close(object)",
+" Close the file returned by an earlier redirect call."),
+
+ JS_FS_HELP_END
+};
+// clang-format on
+
+static bool ospath_isAbsolute(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ if (args.length() != 1 || !args[0].isString()) {
+ JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr,
+ JSSMSG_INVALID_ARGS, "isAbsolute");
+ return false;
+ }
+
+ Rooted<JSLinearString*> str(cx,
+ JS_EnsureLinearString(cx, args[0].toString()));
+ if (!str) {
+ return false;
+ }
+
+ args.rval().setBoolean(IsAbsolutePath(str));
+ return true;
+}
+
+static bool ospath_join(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ if (args.length() < 1) {
+ JS_ReportErrorNumberASCII(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.
+
+ JSStringBuilder buffer(cx);
+ Rooted<JSLinearString*> str(cx);
+
+ for (unsigned i = 0; i < args.length(); i++) {
+ if (!args[i].isString()) {
+ JS_ReportErrorASCII(cx, "join expects string arguments only");
+ return false;
+ }
+
+ str = JS_EnsureLinearString(cx, args[i].toString());
+ if (!str) {
+ return false;
+ }
+
+ if (IsAbsolutePath(str)) {
+ MOZ_ALWAYS_TRUE(buffer.resize(0));
+ } else if (i != 0) {
+ UniqueChars path = JS_EncodeStringToUTF8(cx, str);
+ if (!path) {
+ return false;
+ }
+
+ 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;
+}
+
+// clang-format off
+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
+};
+// clang-format on
+
+static bool os_getenv(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+ if (args.length() < 1) {
+ JS_ReportErrorASCII(cx, "os.getenv requires 1 argument");
+ return false;
+ }
+ RootedString key(cx, ToString(cx, args[0]));
+ if (!key) {
+ return false;
+ }
+ UniqueChars keyBytes = JS_EncodeStringToUTF8(cx, key);
+ if (!keyBytes) {
+ return false;
+ }
+
+ if (const char* valueBytes = getenv(keyBytes.get())) {
+ 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_ReportErrorASCII(cx, "os.getpid takes no arguments");
+ return false;
+ }
+ args.rval().setInt32(getpid());
+ 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
+
+UniqueChars SystemErrorMessage(JSContext* cx, int errnum) {
+#if defined(XP_WIN)
+ wchar_t buffer[200];
+ const wchar_t* errstr = buffer;
+ if (_wcserror_s(buffer, mozilla::ArrayLength(buffer), errnum) != 0) {
+ errstr = L"unknown error";
+ }
+ return JS::EncodeWideToUtf8(cx, errstr);
+#else
+ char buffer[200];
+ const char* errstr = strerror_message(
+ strerror_r(errno, buffer, mozilla::ArrayLength(buffer)), buffer);
+ if (!errstr) {
+ errstr = "unknown error";
+ }
+ return JS::EncodeNarrowToUtf8(cx, errstr);
+#endif
+}
+
+#ifndef __wasi__
+static void ReportSysError(JSContext* cx, const char* prefix) {
+ MOZ_ASSERT(JS::StringIsASCII(prefix));
+
+ if (UniqueChars error = SystemErrorMessage(cx, errno)) {
+ JS_ReportErrorUTF8(cx, "%s: %s", prefix, error.get());
+ }
+}
+
+static bool os_system(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ if (args.length() == 0) {
+ JS_ReportErrorASCII(cx, "os.system requires 1 argument");
+ return false;
+ }
+
+ Rooted<JSString*> str(cx, JS::ToString(cx, args[0]));
+ if (!str) {
+ return false;
+ }
+
+ UniqueChars command = JS_EncodeStringToUTF8(cx, str);
+ if (!command) {
+ return false;
+ }
+
+# ifdef XP_WIN
+ UniqueWideChars wideCommand = JS::EncodeUtf8ToWide(cx, command.get());
+ if (!wideCommand) {
+ return false;
+ }
+
+ // Existing streams must be explicitly flushed or closed before calling
+ // the system() function on Windows.
+ _flushall();
+
+ int result = _wsystem(wideCommand.get());
+# else
+ UniqueChars narrowCommand = JS::EncodeUtf8ToNarrow(cx, command.get());
+ if (!narrowCommand) {
+ return false;
+ }
+
+ int result = system(narrowCommand.get());
+# endif
+ if (result == -1) {
+ ReportSysError(cx, "system call failed");
+ return false;
+ }
+
+ args.rval().setInt32(result);
+ return true;
+}
+
+# ifndef XP_WIN
+static bool os_spawn(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ if (args.length() == 0) {
+ JS_ReportErrorASCII(cx, "os.spawn requires 1 argument");
+ return false;
+ }
+
+ Rooted<JSString*> str(cx, JS::ToString(cx, args[0]));
+ if (!str) {
+ return false;
+ }
+
+ UniqueChars command = JS_EncodeStringToUTF8(cx, str);
+ if (!command) {
+ return false;
+ }
+ UniqueChars narrowCommand = JS::EncodeUtf8ToNarrow(cx, command.get());
+ if (!narrowCommand) {
+ 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] = narrowCommand.get();
+
+ 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_ReportErrorASCII(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_ReportErrorASCII(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
+#endif // !__wasi__
+
+// clang-format off
+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."),
+
+#ifndef __wasi__
+ 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."),
+
+# ifndef XP_WIN
+ 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
+#endif // !__wasi__
+
+ JS_FS_HELP_END
+};
+// clang-format on
+
+bool DefineOS(JSContext* cx, HandleObject global, bool fuzzingSafe,
+ RCFile** shellOut, RCFile** shellErr) {
+ 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;
+ }
+ }
+
+ if (!GenerateInterfaceHelp(cx, osfile, "os.file")) {
+ return false;
+ }
+
+ RootedObject ospath(cx, JS_NewPlainObject(cx));
+ if (!ospath || !JS_DefineFunctionsWithHelp(cx, ospath, ospath_functions) ||
+ !JS_DefineProperty(cx, obj, "path", ospath, 0) ||
+ !GenerateInterfaceHelp(cx, ospath, "os.path")) {
+ return false;
+ }
+
+ if (!GenerateInterfaceHelp(cx, obj, "os")) {
+ return false;
+ }
+
+ ShellContext* scx = GetShellContext(cx);
+ scx->outFilePtr = shellOut;
+ scx->errFilePtr = shellErr;
+
+ // For backwards compatibility, expose various os.file.* functions as
+ // direct methods on the global.
+ struct Export {
+ const char* src;
+ const char* dst;
+ };
+
+ const Export osfile_exports[] = {
+ {"readFile", "read"},
+ {"readFile", "snarf"},
+ {"readRelativeToScript", "readRelativeToScript"},
+ };
+
+ for (auto pair : osfile_exports) {
+ if (!CreateAlias(cx, pair.dst, osfile, pair.src)) {
+ return false;
+ }
+ }
+
+ if (!fuzzingSafe) {
+ const Export unsafe_osfile_exports[] = {{"redirect", "redirect"},
+ {"redirectErr", "redirectErr"}};
+
+ for (auto pair : unsafe_osfile_exports) {
+ if (!CreateAlias(cx, pair.dst, osfile, pair.src)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+} // namespace shell
+} // namespace js