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