diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /js/src/shell/OSObject.cpp | |
parent | Initial commit. (diff) | |
download | thunderbird-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.cpp | 1309 |
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 |