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