tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

OSObject.cpp (34852B)


      1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
      2 * vim: set ts=8 sts=2 et sw=2 tw=80:
      3 * This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 // OSObject.h - os object for exposing posix system calls in the JS shell
      8 
      9 #include "shell/OSObject.h"
     10 
     11 #include "mozilla/ScopeExit.h"
     12 #include "mozilla/TextUtils.h"
     13 
     14 #include <errno.h>
     15 #include <stdlib.h>
     16 #ifdef XP_WIN
     17 #  include <direct.h>
     18 #  include <process.h>
     19 #  include <string.h>
     20 #  include <wchar.h>
     21 #  include "util/WindowsWrapper.h"
     22 #elif __wasi__
     23 #  include <dirent.h>
     24 #  include <sys/types.h>
     25 #  include <unistd.h>
     26 #else
     27 #  include <dirent.h>
     28 #  include <sys/types.h>
     29 #  include <sys/wait.h>
     30 #  include <unistd.h>
     31 #endif
     32 
     33 #include "jsapi.h"
     34 // For JSFunctionSpecWithHelp
     35 #include "jsfriendapi.h"
     36 
     37 #include "gc/GCContext.h"
     38 #include "js/CharacterEncoding.h"
     39 #include "js/Conversions.h"
     40 #include "js/experimental/TypedData.h"  // JS_NewUint8Array
     41 #include "js/Object.h"                  // JS::GetReservedSlot
     42 #include "js/PropertyAndElement.h"      // JS_DefineProperty
     43 #include "js/PropertySpec.h"
     44 #include "js/Value.h"  // JS::Value
     45 #include "js/Wrapper.h"
     46 #include "shell/jsshell.h"
     47 #include "shell/StringUtils.h"
     48 #include "util/GetPidProvider.h"  // getpid()
     49 #include "util/StringBuilder.h"
     50 #include "util/Text.h"
     51 #include "util/WindowsWrapper.h"
     52 #include "vm/JSObject.h"
     53 #include "vm/TypedArrayObject.h"
     54 
     55 #include "vm/JSObject-inl.h"
     56 
     57 #ifdef XP_WIN
     58 #  ifndef PATH_MAX
     59 #    define PATH_MAX (MAX_PATH > _MAX_DIR ? MAX_PATH : _MAX_DIR)
     60 #  endif
     61 #  define getcwd _getcwd
     62 #elif defined(__wasi__)
     63 // Nothing.
     64 #else
     65 #  include <libgen.h>
     66 #endif
     67 
     68 using js::shell::RCFile;
     69 
     70 namespace js {
     71 namespace shell {
     72 
     73 bool IsAbsolutePath(JSLinearString* filename) {
     74  size_t length = filename->length();
     75 
     76 #ifdef XP_WIN
     77  // On Windows there are various forms of absolute paths (see
     78  // http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
     79  // for details):
     80  //
     81  //   "\..."
     82  //   "\\..."
     83  //   "C:\..."
     84  //
     85  // The first two cases are handled by the common test below so we only need a
     86  // specific test for the last one here.
     87 
     88  if (length > 3 && mozilla::IsAsciiAlpha(CharAt(filename, 0)) &&
     89      CharAt(filename, 1) == u':' && CharAt(filename, 2) == u'\\') {
     90    return true;
     91  }
     92 #endif
     93 
     94  return length > 0 && CharAt(filename, 0) == PathSeparator;
     95 }
     96 
     97 static UniqueChars DirectoryName(JSContext* cx, const char* path) {
     98 #ifdef XP_WIN
     99  UniqueWideChars widePath = JS::EncodeUtf8ToWide(cx, path);
    100  if (!widePath) {
    101    return nullptr;
    102  }
    103 
    104  wchar_t dirName[PATH_MAX + 1];
    105  wchar_t* drive = nullptr;
    106  wchar_t* fileName = nullptr;
    107  wchar_t* fileExt = nullptr;
    108 
    109  // The docs say it can return EINVAL, but the compiler says it's void
    110  _wsplitpath(widePath.get(), drive, dirName, fileName, fileExt);
    111 
    112  return JS::EncodeWideToUtf8(cx, dirName);
    113 #else
    114  UniqueChars narrowPath = JS::EncodeUtf8ToNarrow(cx, path);
    115  if (!narrowPath) {
    116    return nullptr;
    117  }
    118 
    119  char dirName[PATH_MAX + 1];
    120  strncpy(dirName, narrowPath.get(), PATH_MAX);
    121  if (dirName[PATH_MAX - 1] != '\0') {
    122    JS_ReportErrorASCII(cx, "Path is too long");
    123    return nullptr;
    124  }
    125 
    126 #  ifdef __wasi__
    127  // dirname() seems not to behave properly with wasi-libc; so we do our own
    128  // simple thing here.
    129  char* p = dirName + strlen(dirName);
    130  bool found = false;
    131  while (p > dirName) {
    132    if (*p == '/') {
    133      found = true;
    134      *p = '\0';
    135      break;
    136    }
    137    p--;
    138  }
    139  if (!found) {
    140    // There's no '/'. Possible cases are the following:
    141    //  * "."
    142    //  * ".."
    143    //  * filename only
    144    //
    145    // dirname() returns "." for all cases.
    146    dirName[0] = '.';
    147    dirName[1] = '\0';
    148  }
    149 #  else
    150  // dirname(dirName) might return dirName, or it might return a
    151  // statically-allocated string
    152  memmove(dirName, dirname(dirName), strlen(dirName) + 1);
    153 #  endif
    154 
    155  return JS::EncodeNarrowToUtf8(cx, dirName);
    156 #endif
    157 }
    158 
    159 /*
    160 * Resolve a (possibly) relative filename to an absolute path. If
    161 * |scriptRelative| is true, then the result will be relative to the directory
    162 * containing the currently-running script, or the current working directory if
    163 * the currently-running script is "-e" (namely, you're using it from the
    164 * command line.) Otherwise, it will be relative to the current working
    165 * directory.
    166 */
    167 JSString* ResolvePath(JSContext* cx, HandleString filenameStr,
    168                      PathResolutionMode resolveMode) {
    169  if (!filenameStr) {
    170 #ifdef XP_WIN
    171    return JS_NewStringCopyZ(cx, "nul");
    172 #elif defined(__wasi__)
    173    MOZ_CRASH("NYI for WASI");
    174    return nullptr;
    175 #else
    176    return JS_NewStringCopyZ(cx, "/dev/null");
    177 #endif
    178  }
    179 
    180  Rooted<JSLinearString*> str(cx, JS_EnsureLinearString(cx, filenameStr));
    181  if (!str) {
    182    return nullptr;
    183  }
    184 
    185  if (IsAbsolutePath(str)) {
    186    return str;
    187  }
    188 
    189  UniqueChars filename = JS_EncodeStringToUTF8(cx, str);
    190  if (!filename) {
    191    return nullptr;
    192  }
    193 
    194  JS::AutoFilename scriptFilename;
    195  if (resolveMode == ScriptRelative) {
    196    // Get the currently executing script's name.
    197 
    198    if (!DescribeScriptedCaller(&scriptFilename, cx) || !scriptFilename.get()) {
    199      JS_ReportErrorASCII(
    200          cx, "cannot resolve path due to hidden or unscripted caller");
    201      return nullptr;
    202    }
    203 
    204    if (strcmp(scriptFilename.get(), "-e") == 0 ||
    205        strcmp(scriptFilename.get(), "typein") == 0) {
    206      resolveMode = RootRelative;
    207    }
    208  }
    209 
    210  UniqueChars path;
    211  if (resolveMode == ScriptRelative) {
    212    path = DirectoryName(cx, scriptFilename.get());
    213  } else {
    214    path = GetCWD(cx);
    215  }
    216 
    217  if (!path) {
    218    return nullptr;
    219  }
    220 
    221  size_t pathLen = strlen(path.get());
    222  size_t filenameLen = strlen(filename.get());
    223  size_t resultLen = pathLen + 1 + filenameLen;
    224 
    225  UniqueChars result = cx->make_pod_array<char>(resultLen + 1);
    226  if (!result) {
    227    return nullptr;
    228  }
    229  memcpy(result.get(), path.get(), pathLen);
    230  result[pathLen] = '/';
    231  memcpy(result.get() + pathLen + 1, filename.get(), filenameLen);
    232  result[pathLen + 1 + filenameLen] = '\0';
    233 
    234  return JS_NewStringCopyUTF8N(cx, JS::UTF8Chars(result.get(), resultLen));
    235 }
    236 
    237 FILE* OpenFile(JSContext* cx, const char* filename, const char* mode) {
    238 #ifdef XP_WIN
    239  // Maximum valid mode string is "w+xb". Longer strings or strings
    240  // containing invalid input lead to undefined behavior.
    241  constexpr size_t MaxValidModeLength = 4;
    242  wchar_t wideMode[MaxValidModeLength + 1] = {0};
    243  for (size_t i = 0; i < MaxValidModeLength && mode[i] != '\0'; i++) {
    244    wideMode[i] = mode[i] & 0x7f;
    245  }
    246 
    247  UniqueWideChars wideFilename = JS::EncodeUtf8ToWide(cx, filename);
    248  if (!wideFilename) {
    249    return nullptr;
    250  }
    251 
    252  FILE* file = _wfopen(wideFilename.get(), wideMode);
    253 #else
    254  UniqueChars narrowFilename = JS::EncodeUtf8ToNarrow(cx, filename);
    255  if (!narrowFilename) {
    256    return nullptr;
    257  }
    258 
    259  FILE* file = fopen(narrowFilename.get(), mode);
    260 #endif
    261 
    262  if (!file) {
    263    if (UniqueChars error = SystemErrorMessage(cx, errno)) {
    264      JS_ReportErrorNumberUTF8(cx, my_GetErrorMessage, nullptr,
    265                               JSSMSG_CANT_OPEN, filename, error.get());
    266    }
    267    return nullptr;
    268  }
    269  return file;
    270 }
    271 
    272 bool ReadFile(JSContext* cx, const char* filename, FILE* file, char* buffer,
    273              size_t length) {
    274  size_t cc = fread(buffer, sizeof(char), length, file);
    275  if (cc != length) {
    276    if (ptrdiff_t(cc) < 0) {
    277      if (UniqueChars error = SystemErrorMessage(cx, errno)) {
    278        JS_ReportErrorNumberUTF8(cx, my_GetErrorMessage, nullptr,
    279                                 JSSMSG_CANT_READ, filename, error.get());
    280      }
    281    } else {
    282      JS_ReportErrorUTF8(cx, "can't read %s: short read", filename);
    283    }
    284    return false;
    285  }
    286  return true;
    287 }
    288 
    289 bool FileSize(JSContext* cx, const char* filename, FILE* file, size_t* size) {
    290  if (fseek(file, 0, SEEK_END) != 0) {
    291    JS_ReportErrorUTF8(cx, "can't seek end of %s", filename);
    292    return false;
    293  }
    294 
    295  size_t len = ftell(file);
    296  if (fseek(file, 0, SEEK_SET) != 0) {
    297    JS_ReportErrorUTF8(cx, "can't seek start of %s", filename);
    298    return false;
    299  }
    300 
    301  *size = len;
    302  return true;
    303 }
    304 
    305 JSObject* FileAsTypedArray(JSContext* cx, JS::HandleString pathnameStr) {
    306  UniqueChars pathname = JS_EncodeStringToUTF8(cx, pathnameStr);
    307  if (!pathname) {
    308    return nullptr;
    309  }
    310 
    311  FILE* file = OpenFile(cx, pathname.get(), "rb");
    312  if (!file) {
    313    return nullptr;
    314  }
    315  AutoCloseFile autoClose(file);
    316 
    317  size_t len;
    318  if (!FileSize(cx, pathname.get(), file, &len)) {
    319    return nullptr;
    320  }
    321 
    322  if (len > ArrayBufferObject::ByteLengthLimit) {
    323    JS_ReportErrorUTF8(cx, "file %s is too large for a Uint8Array",
    324                       pathname.get());
    325    return nullptr;
    326  }
    327 
    328  JS::Rooted<JSObject*> obj(cx, JS_NewUint8Array(cx, len));
    329  if (!obj) {
    330    return nullptr;
    331  }
    332 
    333  js::TypedArrayObject& ta = obj->as<js::TypedArrayObject>();
    334  if (ta.isSharedMemory()) {
    335    // Must opt in to use shared memory.  For now, don't.
    336    //
    337    // (It is incorrect to read into the buffer without
    338    // synchronization since that can create a race.  A
    339    // lock here won't fix it - both sides must
    340    // participate.  So what one must do is to create a
    341    // temporary buffer, read into that, and use a
    342    // race-safe primitive to copy memory into the
    343    // buffer.)
    344    JS_ReportErrorUTF8(cx, "can't read %s: shared memory buffer",
    345                       pathname.get());
    346    return nullptr;
    347  }
    348 
    349  char* buf = static_cast<char*>(ta.dataPointerUnshared());
    350  if (!ReadFile(cx, pathname.get(), file, buf, len)) {
    351    return nullptr;
    352  }
    353 
    354  return obj;
    355 }
    356 
    357 /**
    358 * Return the current working directory or |null| on failure.
    359 */
    360 UniqueChars GetCWD(JSContext* cx) {
    361 #ifdef XP_WIN
    362  wchar_t buffer[PATH_MAX + 1];
    363  const wchar_t* cwd = _wgetcwd(buffer, PATH_MAX);
    364  if (!cwd) {
    365    return nullptr;
    366  }
    367  return JS::EncodeWideToUtf8(cx, buffer);
    368 #else
    369  char buffer[PATH_MAX + 1];
    370  const char* cwd = getcwd(buffer, PATH_MAX);
    371  if (!cwd) {
    372    return nullptr;
    373  }
    374  return JS::EncodeNarrowToUtf8(cx, buffer);
    375 #endif
    376 }
    377 
    378 static bool ReadFile(JSContext* cx, unsigned argc, Value* vp,
    379                     PathResolutionMode resolveMode) {
    380  CallArgs args = CallArgsFromVp(argc, vp);
    381 
    382  if (args.length() < 1 || args.length() > 2) {
    383    JS_ReportErrorNumberASCII(
    384        cx, js::shell::my_GetErrorMessage, nullptr,
    385        args.length() < 1 ? JSSMSG_NOT_ENOUGH_ARGS : JSSMSG_TOO_MANY_ARGS,
    386        "snarf");
    387    return false;
    388  }
    389 
    390  if (!args[0].isString() || (args.length() == 2 && !args[1].isString())) {
    391    JS_ReportErrorNumberASCII(cx, js::shell::my_GetErrorMessage, nullptr,
    392                              JSSMSG_INVALID_ARGS, "snarf");
    393    return false;
    394  }
    395 
    396  JS::Rooted<JSString*> givenPath(cx, args[0].toString());
    397  JS::Rooted<JSString*> str(cx,
    398                            js::shell::ResolvePath(cx, givenPath, resolveMode));
    399  if (!str) {
    400    return false;
    401  }
    402 
    403  if (args.length() > 1) {
    404    JSString* opt = JS::ToString(cx, args[1]);
    405    if (!opt) {
    406      return false;
    407    }
    408    bool match;
    409    if (!JS_StringEqualsLiteral(cx, opt, "binary", &match)) {
    410      return false;
    411    }
    412    if (match) {
    413      JSObject* obj;
    414      if (!(obj = FileAsTypedArray(cx, str))) {
    415        return false;
    416      }
    417      args.rval().setObject(*obj);
    418      return true;
    419    }
    420  }
    421 
    422  if (!(str = FileAsString(cx, str))) {
    423    return false;
    424  }
    425  args.rval().setString(str);
    426  return true;
    427 }
    428 
    429 static bool osfile_readFile(JSContext* cx, unsigned argc, Value* vp) {
    430  return ReadFile(cx, argc, vp, RootRelative);
    431 }
    432 
    433 static bool osfile_readRelativeToScript(JSContext* cx, unsigned argc,
    434                                        Value* vp) {
    435  return ReadFile(cx, argc, vp, ScriptRelative);
    436 }
    437 
    438 static bool ListDir(JSContext* cx, unsigned argc, Value* vp,
    439                    PathResolutionMode resolveMode) {
    440  CallArgs args = CallArgsFromVp(argc, vp);
    441 
    442  if (args.length() != 1) {
    443    JS_ReportErrorASCII(cx, "os.file.listDir requires 1 argument");
    444    return false;
    445  }
    446 
    447  if (!args[0].isString()) {
    448    JS_ReportErrorNumberASCII(cx, js::shell::my_GetErrorMessage, nullptr,
    449                              JSSMSG_INVALID_ARGS, "os.file.listDir");
    450    return false;
    451  }
    452 
    453  RootedString givenPath(cx, args[0].toString());
    454  RootedString str(cx, ResolvePath(cx, givenPath, resolveMode));
    455  if (!str) {
    456    return false;
    457  }
    458 
    459  UniqueChars pathname = JS_EncodeStringToUTF8(cx, str);
    460  if (!pathname) {
    461    JS_ReportErrorASCII(cx, "os.file.listDir cannot convert path to UTF8");
    462    return false;
    463  }
    464 
    465  RootedValueVector elems(cx);
    466  auto append = [&](const char* name) -> bool {
    467    if (!(str = JS_NewStringCopyZ(cx, name))) {
    468      return false;
    469    }
    470    if (!elems.append(StringValue(str))) {
    471      js::ReportOutOfMemory(cx);
    472      return false;
    473    }
    474    return true;
    475  };
    476 
    477 #if defined(XP_UNIX)
    478  {
    479    DIR* dir = opendir(pathname.get());
    480    if (!dir) {
    481      JS_ReportErrorUTF8(cx, "os.file.listDir is unable to open: %s",
    482                         pathname.get());
    483      return false;
    484    }
    485    auto close = mozilla::MakeScopeExit([&] {
    486      if (closedir(dir) != 0) {
    487        MOZ_CRASH("Could not close dir");
    488      }
    489    });
    490 
    491    while (struct dirent* entry = readdir(dir)) {
    492      if (!append(entry->d_name)) {
    493        return false;
    494      }
    495    }
    496  }
    497 #elif defined(XP_WIN)
    498  {
    499    const size_t pathlen = strlen(pathname.get());
    500    Vector<char> pattern(cx);
    501    if (!pattern.append(pathname.get(), pathlen) ||
    502        !pattern.append(PathSeparator) || !pattern.append("*", 2)) {
    503      js::ReportOutOfMemory(cx);
    504      return false;
    505    }
    506 
    507    WIN32_FIND_DATAA FindFileData;
    508    HANDLE hFind = FindFirstFileA(pattern.begin(), &FindFileData);
    509    if (hFind == INVALID_HANDLE_VALUE) {
    510      JS_ReportErrorUTF8(cx, "os.file.listDir is unable to open: %s",
    511                         pathname.get());
    512      return false;
    513    }
    514    auto close = mozilla::MakeScopeExit([&] {
    515      if (!FindClose(hFind)) {
    516        MOZ_CRASH("Could not close Find");
    517      }
    518    });
    519    for (bool found = (hFind != INVALID_HANDLE_VALUE); found;
    520         found = FindNextFileA(hFind, &FindFileData)) {
    521      if (!append(FindFileData.cFileName)) {
    522        return false;
    523      }
    524    }
    525  }
    526 #endif
    527 
    528  JSObject* array = JS::NewArrayObject(cx, elems);
    529  if (!array) {
    530    return false;
    531  }
    532 
    533  args.rval().setObject(*array);
    534  return true;
    535 }
    536 
    537 static bool osfile_listDir(JSContext* cx, unsigned argc, Value* vp) {
    538  return ListDir(cx, argc, vp, RootRelative);
    539 }
    540 
    541 static bool osfile_listDirRelativeToScript(JSContext* cx, unsigned argc,
    542                                           Value* vp) {
    543  return ListDir(cx, argc, vp, ScriptRelative);
    544 }
    545 
    546 static bool osfile_writeTypedArrayToFile(JSContext* cx, unsigned argc,
    547                                         Value* vp) {
    548  CallArgs args = CallArgsFromVp(argc, vp);
    549 
    550  if (args.length() != 2 || !args[0].isString() || !args[1].isObject() ||
    551      !args[1].toObject().is<TypedArrayObject>()) {
    552    JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr,
    553                              JSSMSG_INVALID_ARGS, "writeTypedArrayToFile");
    554    return false;
    555  }
    556 
    557  RootedString givenPath(cx, args[0].toString());
    558  RootedString str(cx, ResolvePath(cx, givenPath, RootRelative));
    559  if (!str) {
    560    return false;
    561  }
    562 
    563  UniqueChars filename = JS_EncodeStringToUTF8(cx, str);
    564  if (!filename) {
    565    return false;
    566  }
    567 
    568  FILE* file = OpenFile(cx, filename.get(), "wb");
    569  if (!file) {
    570    return false;
    571  }
    572  AutoCloseFile autoClose(file);
    573 
    574  TypedArrayObject* obj = &args[1].toObject().as<TypedArrayObject>();
    575 
    576  if (obj->isSharedMemory()) {
    577    // Must opt in to use shared memory.  For now, don't.
    578    //
    579    // See further comments in FileAsTypedArray, above.
    580    JS_ReportErrorUTF8(cx, "can't write %s: shared memory buffer",
    581                       filename.get());
    582    return false;
    583  }
    584  void* buf = obj->dataPointerUnshared();
    585  size_t length = obj->length().valueOr(0);
    586  if (fwrite(buf, obj->bytesPerElement(), length, file) != length ||
    587      !autoClose.release()) {
    588    JS_ReportErrorUTF8(cx, "can't write %s", filename.get());
    589    return false;
    590  }
    591 
    592  args.rval().setUndefined();
    593  return true;
    594 }
    595 
    596 /* static */
    597 RCFile* RCFile::create(JSContext* cx, const char* filename, const char* mode) {
    598  FILE* fp = OpenFile(cx, filename, mode);
    599  if (!fp) {
    600    return nullptr;
    601  }
    602 
    603  RCFile* file = cx->new_<RCFile>(fp);
    604  if (!file) {
    605    fclose(fp);
    606    return nullptr;
    607  }
    608 
    609  return file;
    610 }
    611 
    612 void RCFile::close() {
    613  if (fp) {
    614    fclose(fp);
    615  }
    616  fp = nullptr;
    617 }
    618 
    619 bool RCFile::release() {
    620  if (--numRefs) {
    621    return false;
    622  }
    623  this->close();
    624  return true;
    625 }
    626 
    627 class FileObject : public NativeObject {
    628  enum : uint32_t { FILE_SLOT = 0, NUM_SLOTS };
    629 
    630 public:
    631  static const JSClass class_;
    632 
    633  static FileObject* create(JSContext* cx, RCFile* file) {
    634    FileObject* obj = js::NewBuiltinClassInstance<FileObject>(cx);
    635    if (!obj) {
    636      return nullptr;
    637    }
    638 
    639    InitReservedSlot(obj, FILE_SLOT, file, MemoryUse::FileObjectFile);
    640    file->acquire();
    641    return obj;
    642  }
    643 
    644  static void finalize(JS::GCContext* gcx, JSObject* obj) {
    645    FileObject* fileObj = &obj->as<FileObject>();
    646    RCFile* file = fileObj->rcFile();
    647    gcx->removeCellMemory(obj, sizeof(*file), MemoryUse::FileObjectFile);
    648    if (file->release()) {
    649      gcx->deleteUntracked(file);
    650    }
    651  }
    652 
    653  bool isOpen() {
    654    RCFile* file = rcFile();
    655    return file && file->isOpen();
    656  }
    657 
    658  void close() {
    659    if (!isOpen()) {
    660      return;
    661    }
    662    rcFile()->close();
    663  }
    664 
    665  RCFile* rcFile() {
    666    return reinterpret_cast<RCFile*>(
    667        JS::GetReservedSlot(this, FILE_SLOT).toPrivate());
    668  }
    669 };
    670 
    671 static const JSClassOps FileObjectClassOps = {
    672    nullptr,               // addProperty
    673    nullptr,               // delProperty
    674    nullptr,               // enumerate
    675    nullptr,               // newEnumerate
    676    nullptr,               // resolve
    677    nullptr,               // mayResolve
    678    FileObject::finalize,  // finalize
    679    nullptr,               // call
    680    nullptr,               // construct
    681    nullptr,               // trace
    682 };
    683 
    684 const JSClass FileObject::class_ = {
    685    "File",
    686    JSCLASS_HAS_RESERVED_SLOTS(FileObject::NUM_SLOTS) |
    687        JSCLASS_FOREGROUND_FINALIZE,
    688    &FileObjectClassOps,
    689 };
    690 
    691 static FileObject* redirect(JSContext* cx, HandleString relFilename,
    692                            RCFile** globalFile) {
    693  RootedString filename(cx, ResolvePath(cx, relFilename, RootRelative));
    694  if (!filename) {
    695    return nullptr;
    696  }
    697  UniqueChars filenameABS = JS_EncodeStringToUTF8(cx, filename);
    698  if (!filenameABS) {
    699    return nullptr;
    700  }
    701  RCFile* file = RCFile::create(cx, filenameABS.get(), "wb");
    702  if (!file) {
    703    return nullptr;
    704  }
    705 
    706  // Grant the global gOutFile ownership of the new file, release ownership
    707  // of its old file, and return a FileObject owning the old file.
    708  file->acquire();  // Global owner of new file
    709 
    710  FileObject* fileObj =
    711      FileObject::create(cx, *globalFile);  // Newly created owner of old file
    712  if (!fileObj) {
    713    file->release();
    714    return nullptr;
    715  }
    716 
    717  (*globalFile)->release();  // Release (global) ownership of old file.
    718  *globalFile = file;
    719 
    720  return fileObj;
    721 }
    722 
    723 static bool Redirect(JSContext* cx, const CallArgs& args, RCFile** outFile) {
    724  if (args.length() > 1) {
    725    JS_ReportErrorNumberASCII(cx, js::shell::my_GetErrorMessage, nullptr,
    726                              JSSMSG_INVALID_ARGS, "redirect");
    727    return false;
    728  }
    729 
    730  RCFile* oldFile = *outFile;
    731  RootedObject oldFileObj(cx, FileObject::create(cx, oldFile));
    732  if (!oldFileObj) {
    733    return false;
    734  }
    735 
    736  if (args.get(0).isUndefined()) {
    737    args.rval().setObject(*oldFileObj);
    738    return true;
    739  }
    740 
    741  if (args[0].isObject()) {
    742    Rooted<FileObject*> fileObj(cx,
    743                                args[0].toObject().maybeUnwrapIf<FileObject>());
    744    if (!fileObj) {
    745      JS_ReportErrorNumberASCII(cx, js::shell::my_GetErrorMessage, nullptr,
    746                                JSSMSG_INVALID_ARGS, "redirect");
    747      return false;
    748    }
    749 
    750    // Passed in a FileObject. Create a FileObject for the previous
    751    // global file, and set the global file to the passed-in one.
    752    *outFile = fileObj->rcFile();
    753    (*outFile)->acquire();
    754    oldFile->release();
    755 
    756    args.rval().setObject(*oldFileObj);
    757    return true;
    758  }
    759 
    760  RootedString filename(cx);
    761  if (!args[0].isNull()) {
    762    filename = JS::ToString(cx, args[0]);
    763    if (!filename) {
    764      return false;
    765    }
    766  }
    767 
    768  if (!redirect(cx, filename, outFile)) {
    769    return false;
    770  }
    771 
    772  args.rval().setObject(*oldFileObj);
    773  return true;
    774 }
    775 
    776 static bool osfile_redirectOutput(JSContext* cx, unsigned argc, Value* vp) {
    777  CallArgs args = CallArgsFromVp(argc, vp);
    778  ShellContext* scx = GetShellContext(cx);
    779  return Redirect(cx, args, scx->outFilePtr);
    780 }
    781 
    782 static bool osfile_redirectError(JSContext* cx, unsigned argc, Value* vp) {
    783  CallArgs args = CallArgsFromVp(argc, vp);
    784  ShellContext* scx = GetShellContext(cx);
    785  return Redirect(cx, args, scx->errFilePtr);
    786 }
    787 
    788 static bool osfile_close(JSContext* cx, unsigned argc, Value* vp) {
    789  CallArgs args = CallArgsFromVp(argc, vp);
    790 
    791  Rooted<FileObject*> fileObj(cx);
    792  if (args.get(0).isObject()) {
    793    fileObj = args[0].toObject().maybeUnwrapIf<FileObject>();
    794  }
    795 
    796  if (!fileObj) {
    797    JS_ReportErrorNumberASCII(cx, js::shell::my_GetErrorMessage, nullptr,
    798                              JSSMSG_INVALID_ARGS, "close");
    799    return false;
    800  }
    801 
    802  fileObj->close();
    803 
    804  args.rval().setUndefined();
    805  return true;
    806 }
    807 
    808 // clang-format off
    809 static const JSFunctionSpecWithHelp osfile_functions[] = {
    810    JS_FN_HELP("readFile", osfile_readFile, 1, 0,
    811 "readFile(filename, [\"binary\"])",
    812 "  Read entire contents of filename. Returns a string, unless \"binary\" is passed\n"
    813 "  as the second argument, in which case it returns a Uint8Array. Filename is\n"
    814 "  relative to the current working directory."),
    815 
    816    JS_FN_HELP("readRelativeToScript", osfile_readRelativeToScript, 1, 0,
    817 "readRelativeToScript(filename, [\"binary\"])",
    818 "  Read filename into returned string. Filename is relative to the directory\n"
    819 "  containing the current script."),
    820 
    821    JS_FN_HELP("listDir", osfile_listDir, 1, 0,
    822 "listDir(dirname)",
    823 "  Read entire contents of a directory. The \"dirname\" parameter is relate to the\n"
    824 "  current working directory. Returns a list of filenames within the given directory.\n"
    825 "  Note that \".\" and \"..\" are also listed."),
    826 
    827    JS_FN_HELP("listDirRelativeToScript", osfile_listDirRelativeToScript, 1, 0,
    828 "listDirRelativeToScript(dirname)",
    829 "  Same as \"listDir\" except that the \"dirname\" is relative to the directory\n"
    830 "  containing the current script."),
    831 
    832    JS_FS_HELP_END
    833 };
    834 // clang-format on
    835 
    836 // clang-format off
    837 static const JSFunctionSpecWithHelp osfile_unsafe_functions[] = {
    838    JS_FN_HELP("writeTypedArrayToFile", osfile_writeTypedArrayToFile, 2, 0,
    839 "writeTypedArrayToFile(filename, data)",
    840 "  Write the contents of a typed array to the named file."),
    841 
    842    JS_FN_HELP("redirect", osfile_redirectOutput, 1, 0,
    843 "redirect([path-or-object])",
    844 "  Redirect print() output to the named file.\n"
    845 "   Return an opaque object representing the previous destination, which\n"
    846 "   may be passed into redirect() later to restore the output."),
    847 
    848    JS_FN_HELP("redirectErr", osfile_redirectError, 1, 0,
    849 "redirectErr([path-or-object])",
    850 "  Same as redirect(), but for printErr"),
    851 
    852    JS_FN_HELP("close", osfile_close, 1, 0,
    853 "close(object)",
    854 "  Close the file returned by an earlier redirect call."),
    855 
    856    JS_FS_HELP_END
    857 };
    858 // clang-format on
    859 
    860 static bool ospath_isAbsolute(JSContext* cx, unsigned argc, Value* vp) {
    861  CallArgs args = CallArgsFromVp(argc, vp);
    862 
    863  if (args.length() != 1 || !args[0].isString()) {
    864    JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr,
    865                              JSSMSG_INVALID_ARGS, "isAbsolute");
    866    return false;
    867  }
    868 
    869  Rooted<JSLinearString*> str(cx,
    870                              JS_EnsureLinearString(cx, args[0].toString()));
    871  if (!str) {
    872    return false;
    873  }
    874 
    875  args.rval().setBoolean(IsAbsolutePath(str));
    876  return true;
    877 }
    878 
    879 static bool ospath_join(JSContext* cx, unsigned argc, Value* vp) {
    880  CallArgs args = CallArgsFromVp(argc, vp);
    881 
    882  if (args.length() < 1) {
    883    JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr,
    884                              JSSMSG_INVALID_ARGS, "join");
    885    return false;
    886  }
    887 
    888  // This function doesn't take into account some aspects of Windows paths,
    889  // e.g. the drive letter is always reset when an absolute path is appended.
    890 
    891  JSStringBuilder buffer(cx);
    892  Rooted<JSLinearString*> str(cx);
    893 
    894  for (unsigned i = 0; i < args.length(); i++) {
    895    if (!args[i].isString()) {
    896      JS_ReportErrorASCII(cx, "join expects string arguments only");
    897      return false;
    898    }
    899 
    900    str = JS_EnsureLinearString(cx, args[i].toString());
    901    if (!str) {
    902      return false;
    903    }
    904 
    905    if (IsAbsolutePath(str)) {
    906      buffer.clear();
    907    } else if (i != 0) {
    908      UniqueChars path = JS_EncodeStringToUTF8(cx, str);
    909      if (!path) {
    910        return false;
    911      }
    912 
    913      if (!buffer.append(PathSeparator)) {
    914        return false;
    915      }
    916    }
    917 
    918    if (!buffer.append(args[i].toString())) {
    919      return false;
    920    }
    921  }
    922 
    923  JSString* result = buffer.finishString();
    924  if (!result) {
    925    return false;
    926  }
    927 
    928  args.rval().setString(result);
    929  return true;
    930 }
    931 
    932 // clang-format off
    933 static const JSFunctionSpecWithHelp ospath_functions[] = {
    934    JS_FN_HELP("isAbsolute", ospath_isAbsolute, 1, 0,
    935 "isAbsolute(path)",
    936 "  Return whether the given path is absolute."),
    937 
    938    JS_FN_HELP("join", ospath_join, 1, 0,
    939 "join(paths...)",
    940 "  Join one or more path components in a platform independent way."),
    941 
    942    JS_FS_HELP_END
    943 };
    944 // clang-format on
    945 
    946 static bool os_getenv(JSContext* cx, unsigned argc, Value* vp) {
    947  CallArgs args = CallArgsFromVp(argc, vp);
    948  if (args.length() < 1) {
    949    JS_ReportErrorASCII(cx, "os.getenv requires 1 argument");
    950    return false;
    951  }
    952  RootedString key(cx, ToString(cx, args[0]));
    953  if (!key) {
    954    return false;
    955  }
    956  UniqueChars keyBytes = JS_EncodeStringToUTF8(cx, key);
    957  if (!keyBytes) {
    958    return false;
    959  }
    960 
    961  if (const char* valueBytes = getenv(keyBytes.get())) {
    962    RootedString value(cx, JS_NewStringCopyZ(cx, valueBytes));
    963    if (!value) {
    964      return false;
    965    }
    966    args.rval().setString(value);
    967  } else {
    968    args.rval().setUndefined();
    969  }
    970  return true;
    971 }
    972 
    973 static bool os_getpid(JSContext* cx, unsigned argc, Value* vp) {
    974  CallArgs args = CallArgsFromVp(argc, vp);
    975  if (args.length() != 0) {
    976    JS_ReportErrorASCII(cx, "os.getpid takes no arguments");
    977    return false;
    978  }
    979  args.rval().setInt32(getpid());
    980  return true;
    981 }
    982 
    983 #if !defined(XP_WIN)
    984 
    985 // There are two possible definitions of strerror_r floating around. The GNU
    986 // one returns a char* which may or may not be the buffer you passed in. The
    987 // other one returns an integer status code, and always writes the result into
    988 // the provided buffer.
    989 
    990 inline char* strerror_message(int result, char* buffer) {
    991  return result == 0 ? buffer : nullptr;
    992 }
    993 
    994 inline char* strerror_message(char* result, char* buffer) { return result; }
    995 
    996 #endif
    997 
    998 UniqueChars SystemErrorMessage(JSContext* cx, int errnum) {
    999 #if defined(XP_WIN)
   1000  wchar_t buffer[200];
   1001  const wchar_t* errstr = buffer;
   1002  if (_wcserror_s(buffer, std::size(buffer), errnum) != 0) {
   1003    errstr = L"unknown error";
   1004  }
   1005  return JS::EncodeWideToUtf8(cx, errstr);
   1006 #else
   1007  char buffer[200];
   1008  const char* errstr =
   1009      strerror_message(strerror_r(errno, buffer, std::size(buffer)), buffer);
   1010  if (!errstr) {
   1011    errstr = "unknown error";
   1012  }
   1013  return JS::EncodeNarrowToUtf8(cx, errstr);
   1014 #endif
   1015 }
   1016 
   1017 #ifndef __wasi__
   1018 static void ReportSysError(JSContext* cx, const char* prefix) {
   1019  MOZ_ASSERT(JS::StringIsASCII(prefix));
   1020 
   1021  if (UniqueChars error = SystemErrorMessage(cx, errno)) {
   1022    JS_ReportErrorUTF8(cx, "%s: %s", prefix, error.get());
   1023  }
   1024 }
   1025 
   1026 static bool os_system(JSContext* cx, unsigned argc, Value* vp) {
   1027  CallArgs args = CallArgsFromVp(argc, vp);
   1028 
   1029  if (args.length() == 0) {
   1030    JS_ReportErrorASCII(cx, "os.system requires 1 argument");
   1031    return false;
   1032  }
   1033 
   1034  Rooted<JSString*> str(cx, JS::ToString(cx, args[0]));
   1035  if (!str) {
   1036    return false;
   1037  }
   1038 
   1039  UniqueChars command = JS_EncodeStringToUTF8(cx, str);
   1040  if (!command) {
   1041    return false;
   1042  }
   1043 
   1044 #  ifdef XP_WIN
   1045  UniqueWideChars wideCommand = JS::EncodeUtf8ToWide(cx, command.get());
   1046  if (!wideCommand) {
   1047    return false;
   1048  }
   1049 
   1050  // Existing streams must be explicitly flushed or closed before calling
   1051  // the system() function on Windows.
   1052  _flushall();
   1053 
   1054  int result = _wsystem(wideCommand.get());
   1055 #  else
   1056  UniqueChars narrowCommand = JS::EncodeUtf8ToNarrow(cx, command.get());
   1057  if (!narrowCommand) {
   1058    return false;
   1059  }
   1060 
   1061  int result = system(narrowCommand.get());
   1062 #  endif
   1063  if (result == -1) {
   1064    ReportSysError(cx, "system call failed");
   1065    return false;
   1066  }
   1067 
   1068  args.rval().setInt32(result);
   1069  return true;
   1070 }
   1071 
   1072 #  ifndef XP_WIN
   1073 static bool os_spawn(JSContext* cx, unsigned argc, Value* vp) {
   1074  CallArgs args = CallArgsFromVp(argc, vp);
   1075 
   1076  if (args.length() == 0) {
   1077    JS_ReportErrorASCII(cx, "os.spawn requires 1 argument");
   1078    return false;
   1079  }
   1080 
   1081  Rooted<JSString*> str(cx, JS::ToString(cx, args[0]));
   1082  if (!str) {
   1083    return false;
   1084  }
   1085 
   1086  UniqueChars command = JS_EncodeStringToUTF8(cx, str);
   1087  if (!command) {
   1088    return false;
   1089  }
   1090  UniqueChars narrowCommand = JS::EncodeUtf8ToNarrow(cx, command.get());
   1091  if (!narrowCommand) {
   1092    return false;
   1093  }
   1094 
   1095  int32_t childPid = fork();
   1096  if (childPid == -1) {
   1097    ReportSysError(cx, "fork failed");
   1098    return false;
   1099  }
   1100 
   1101  if (childPid) {
   1102    args.rval().setInt32(childPid);
   1103    return true;
   1104  }
   1105 
   1106  // We are in the child
   1107 
   1108  const char* cmd[] = {"sh", "-c", nullptr, nullptr};
   1109  cmd[2] = narrowCommand.get();
   1110 
   1111  execvp("sh", (char* const*)cmd);
   1112  exit(1);
   1113 }
   1114 
   1115 static bool os_kill(JSContext* cx, unsigned argc, Value* vp) {
   1116  CallArgs args = CallArgsFromVp(argc, vp);
   1117  int32_t pid;
   1118  if (args.length() < 1) {
   1119    JS_ReportErrorASCII(cx, "os.kill requires 1 argument");
   1120    return false;
   1121  }
   1122  if (!JS::ToInt32(cx, args[0], &pid)) {
   1123    return false;
   1124  }
   1125 
   1126  // It is too easy to kill yourself accidentally with os.kill("goose").
   1127  if (pid == 0 && !args[0].isInt32()) {
   1128    JS_ReportErrorASCII(cx, "os.kill requires numeric pid");
   1129    return false;
   1130  }
   1131 
   1132  int signal = SIGINT;
   1133  if (args.length() > 1) {
   1134    if (!JS::ToInt32(cx, args[1], &signal)) {
   1135      return false;
   1136    }
   1137  }
   1138 
   1139  int status = kill(pid, signal);
   1140  if (status == -1) {
   1141    ReportSysError(cx, "kill failed");
   1142    return false;
   1143  }
   1144 
   1145  args.rval().setUndefined();
   1146  return true;
   1147 }
   1148 
   1149 static bool os_waitpid(JSContext* cx, unsigned argc, Value* vp) {
   1150  CallArgs args = CallArgsFromVp(argc, vp);
   1151 
   1152  int32_t pid;
   1153  if (args.length() == 0) {
   1154    pid = -1;
   1155  } else {
   1156    if (!JS::ToInt32(cx, args[0], &pid)) {
   1157      return false;
   1158    }
   1159  }
   1160 
   1161  bool nohang = false;
   1162  if (args.length() >= 2) {
   1163    nohang = JS::ToBoolean(args[1]);
   1164  }
   1165 
   1166  int status = 0;
   1167  pid_t result = waitpid(pid, &status, nohang ? WNOHANG : 0);
   1168  if (result == -1) {
   1169    ReportSysError(cx, "os.waitpid failed");
   1170    return false;
   1171  }
   1172 
   1173  RootedObject info(cx, JS_NewPlainObject(cx));
   1174  if (!info) {
   1175    return false;
   1176  }
   1177 
   1178  RootedValue v(cx);
   1179  if (result != 0) {
   1180    v.setInt32(result);
   1181    if (!JS_DefineProperty(cx, info, "pid", v, JSPROP_ENUMERATE)) {
   1182      return false;
   1183    }
   1184    if (WIFEXITED(status)) {
   1185      v.setInt32(WEXITSTATUS(status));
   1186      if (!JS_DefineProperty(cx, info, "exitStatus", v, JSPROP_ENUMERATE)) {
   1187        return false;
   1188      }
   1189    }
   1190  }
   1191 
   1192  args.rval().setObject(*info);
   1193  return true;
   1194 }
   1195 #  endif
   1196 #endif  // !__wasi__
   1197 
   1198 // clang-format off
   1199 static const JSFunctionSpecWithHelp os_functions[] = {
   1200    JS_FN_HELP("getenv", os_getenv, 1, 0,
   1201 "getenv(variable)",
   1202 "  Get the value of an environment variable."),
   1203 
   1204    JS_FN_HELP("getpid", os_getpid, 0, 0,
   1205 "getpid()",
   1206 "  Return the current process id."),
   1207 
   1208 #ifndef __wasi__
   1209    JS_FN_HELP("system", os_system, 1, 0,
   1210 "system(command)",
   1211 "  Execute command on the current host, returning result code or throwing an\n"
   1212 "  exception on failure."),
   1213 
   1214 #  ifndef XP_WIN
   1215    JS_FN_HELP("spawn", os_spawn, 1, 0,
   1216 "spawn(command)",
   1217 "  Start up a separate process running the given command. Returns the pid."),
   1218 
   1219    JS_FN_HELP("kill", os_kill, 1, 0,
   1220 "kill(pid[, signal])",
   1221 "  Send a signal to the given pid. The default signal is SIGINT. The signal\n"
   1222 "  passed in must be numeric, if given."),
   1223 
   1224    JS_FN_HELP("waitpid", os_waitpid, 1, 0,
   1225 "waitpid(pid[, nohang])",
   1226 "  Calls waitpid(). 'nohang' is a boolean indicating whether to pass WNOHANG.\n"
   1227 "  The return value is an object containing a 'pid' field, if a process was waitable\n"
   1228 "  and an 'exitStatus' field if a pid exited."),
   1229 #  endif
   1230 #endif  // !__wasi__
   1231 
   1232    JS_FS_HELP_END
   1233 };
   1234 // clang-format on
   1235 
   1236 bool DefineOS(JSContext* cx, HandleObject global, bool fuzzingSafe,
   1237              RCFile** shellOut, RCFile** shellErr) {
   1238  RootedObject obj(cx, JS_NewPlainObject(cx));
   1239  if (!obj || !JS_DefineProperty(cx, global, "os", obj, 0)) {
   1240    return false;
   1241  }
   1242 
   1243  if (!fuzzingSafe) {
   1244    if (!JS_DefineFunctionsWithHelp(cx, obj, os_functions)) {
   1245      return false;
   1246    }
   1247  }
   1248 
   1249  RootedObject osfile(cx, JS_NewPlainObject(cx));
   1250  if (!osfile || !JS_DefineFunctionsWithHelp(cx, osfile, osfile_functions) ||
   1251      !JS_DefineProperty(cx, obj, "file", osfile, 0)) {
   1252    return false;
   1253  }
   1254 
   1255  if (!fuzzingSafe) {
   1256    if (!JS_DefineFunctionsWithHelp(cx, osfile, osfile_unsafe_functions)) {
   1257      return false;
   1258    }
   1259  }
   1260 
   1261  if (!GenerateInterfaceHelp(cx, osfile, "os.file")) {
   1262    return false;
   1263  }
   1264 
   1265  RootedObject ospath(cx, JS_NewPlainObject(cx));
   1266  if (!ospath || !JS_DefineFunctionsWithHelp(cx, ospath, ospath_functions) ||
   1267      !JS_DefineProperty(cx, obj, "path", ospath, 0) ||
   1268      !GenerateInterfaceHelp(cx, ospath, "os.path")) {
   1269    return false;
   1270  }
   1271 
   1272  if (!GenerateInterfaceHelp(cx, obj, "os")) {
   1273    return false;
   1274  }
   1275 
   1276  ShellContext* scx = GetShellContext(cx);
   1277  scx->outFilePtr = shellOut;
   1278  scx->errFilePtr = shellErr;
   1279 
   1280  // For backwards compatibility, expose various os.file.* functions as
   1281  // direct methods on the global.
   1282  struct Export {
   1283    const char* src;
   1284    const char* dst;
   1285  };
   1286 
   1287  const Export osfile_exports[] = {
   1288      {"readFile", "read"},
   1289      {"readFile", "snarf"},
   1290      {"readRelativeToScript", "readRelativeToScript"},
   1291  };
   1292 
   1293  for (auto pair : osfile_exports) {
   1294    if (!CreateAlias(cx, pair.dst, osfile, pair.src)) {
   1295      return false;
   1296    }
   1297  }
   1298 
   1299  if (!fuzzingSafe) {
   1300    const Export unsafe_osfile_exports[] = {{"redirect", "redirect"},
   1301                                            {"redirectErr", "redirectErr"}};
   1302 
   1303    for (auto pair : unsafe_osfile_exports) {
   1304      if (!CreateAlias(cx, pair.dst, osfile, pair.src)) {
   1305        return false;
   1306      }
   1307    }
   1308  }
   1309 
   1310  return true;
   1311 }
   1312 
   1313 }  // namespace shell
   1314 }  // namespace js