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