diff options
Diffstat (limited to '')
-rw-r--r-- | js/xpconnect/src/XPCShellImpl.cpp | 1539 |
1 files changed, 1539 insertions, 0 deletions
diff --git a/js/xpconnect/src/XPCShellImpl.cpp b/js/xpconnect/src/XPCShellImpl.cpp new file mode 100644 index 0000000000..124c2ed37d --- /dev/null +++ b/js/xpconnect/src/XPCShellImpl.cpp @@ -0,0 +1,1539 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsXULAppAPI.h" +#include "jsapi.h" +#include "jsfriendapi.h" +#include "js/Array.h" // JS::NewArrayObject +#include "js/CallAndConstruct.h" // JS_CallFunctionValue +#include "js/CharacterEncoding.h" +#include "js/CompilationAndEvaluation.h" // JS::Evaluate +#include "js/ContextOptions.h" +#include "js/Printf.h" +#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineFunctions, JS_DefineProperty +#include "js/PropertySpec.h" +#include "js/SourceText.h" // JS::SourceText +#include "mozilla/ChaosMode.h" +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/IOInterposer.h" +#include "mozilla/Preferences.h" +#include "mozilla/Utf8.h" // mozilla::Utf8Unit +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsExceptionHandler.h" +#include "nsIServiceManager.h" +#include "nsIFile.h" +#include "nsString.h" +#include "nsIDirectoryService.h" +#include "nsDirectoryServiceDefs.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nscore.h" +#include "nsArrayEnumerator.h" +#include "nsCOMArray.h" +#include "nsDirectoryServiceUtils.h" +#include "nsCOMPtr.h" +#include "nsJSPrincipals.h" +#include "nsJSUtils.h" +#include "xpcpublic.h" +#include "xpcprivate.h" +#include "BackstagePass.h" +#include "nsIScriptSecurityManager.h" +#include "nsIPrincipal.h" +#include "nsJSUtils.h" + +#include "nsIXULRuntime.h" +#include "nsIAppStartup.h" +#include "Components.h" +#include "ProfilerControl.h" + +#ifdef ANDROID +# include <android/log.h> +# include "XREShellData.h" +#endif + +#ifdef XP_WIN +# include "mozilla/mscom/ProcessRuntime.h" +# include "mozilla/ScopeExit.h" +# include "mozilla/widget/AudioSession.h" +# include "mozilla/WinDllServices.h" +# include "mozilla/WindowsBCryptInitialization.h" +# include <windows.h> +# if defined(MOZ_SANDBOX) +# include "XREShellData.h" +# include "sandboxBroker.h" +# endif +#endif + +#ifdef MOZ_CODE_COVERAGE +# include "mozilla/CodeCoverageHandler.h" +#endif + +// all this crap is needed to do the interactive shell stuff +#include <stdlib.h> +#include <errno.h> +#ifdef HAVE_IO_H +# include <io.h> /* for isatty() */ +#endif +#ifdef HAVE_UNISTD_H +# include <unistd.h> /* for isatty() */ +#endif + +#ifdef ENABLE_TESTS +# include "xpctest_private.h" +#endif + +// Fuzzing support for XPC runtime fuzzing +#ifdef FUZZING_INTERFACES +# include "xpcrtfuzzing/xpcrtfuzzing.h" +# include "XREShellData.h" +static bool fuzzDoDebug = !!getenv("MOZ_FUZZ_DEBUG"); +static bool fuzzHaveModule = !!getenv("FUZZER"); +#endif // FUZZING_INTERFACES + +using namespace mozilla; +using namespace JS; +using mozilla::dom::AutoEntryScript; +using mozilla::dom::AutoJSAPI; + +class XPCShellDirProvider : public nsIDirectoryServiceProvider2 { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIDIRECTORYSERVICEPROVIDER + NS_DECL_NSIDIRECTORYSERVICEPROVIDER2 + + XPCShellDirProvider() = default; + ~XPCShellDirProvider() = default; + + // The platform resource folder + void SetGREDirs(nsIFile* greDir); + void ClearGREDirs() { + mGREDir = nullptr; + mGREBinDir = nullptr; + } + // The application resource folder + void SetAppDir(nsIFile* appFile); + void ClearAppDir() { mAppDir = nullptr; } + // The app executable + void SetAppFile(nsIFile* appFile); + void ClearAppFile() { mAppFile = nullptr; } + + private: + nsCOMPtr<nsIFile> mGREDir; + nsCOMPtr<nsIFile> mGREBinDir; + nsCOMPtr<nsIFile> mAppDir; + nsCOMPtr<nsIFile> mAppFile; +}; + +#ifdef XP_WIN +class MOZ_STACK_CLASS AutoAudioSession { + public: + AutoAudioSession() { widget::StartAudioSession(); } + + ~AutoAudioSession() { widget::StopAudioSession(); } +}; +#endif + +#define EXITCODE_RUNTIME_ERROR 3 +#define EXITCODE_FILE_NOT_FOUND 4 + +static FILE* gOutFile = nullptr; +static FILE* gErrFile = nullptr; +static FILE* gInFile = nullptr; + +static int gExitCode = 0; +static bool gQuitting = false; +static bool reportWarnings = true; +static bool compileOnly = false; + +static JSPrincipals* gJSPrincipals = nullptr; +static nsAutoString* gWorkingDirectory = nullptr; + +static bool GetLocationProperty(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + if (!args.thisv().isObject()) { + JS_ReportErrorASCII(cx, "Unexpected this value for GetLocationProperty"); + return false; + } +#if !defined(XP_WIN) && !defined(XP_UNIX) + // XXX: your platform should really implement this + return false; +#else + JS::AutoFilename filename; + if (JS::DescribeScriptedCaller(cx, &filename) && filename.get()) { + NS_ConvertUTF8toUTF16 filenameString(filename.get()); + +# if defined(XP_WIN) + // replace forward slashes with backslashes, + // since nsLocalFileWin chokes on them + char16_t* start = filenameString.BeginWriting(); + char16_t* end = filenameString.EndWriting(); + + while (start != end) { + if (*start == L'/') { + *start = L'\\'; + } + start++; + } +# endif + + nsCOMPtr<nsIFile> location; + nsresult rv = + NS_NewLocalFile(filenameString, false, getter_AddRefs(location)); + + if (!location && gWorkingDirectory) { + // could be a relative path, try appending it to the cwd + // and then normalize + nsAutoString absolutePath(*gWorkingDirectory); + absolutePath.Append(filenameString); + + rv = NS_NewLocalFile(absolutePath, false, getter_AddRefs(location)); + } + + if (location) { + bool symlink; + // don't normalize symlinks, because that's kind of confusing + if (NS_SUCCEEDED(location->IsSymlink(&symlink)) && !symlink) + location->Normalize(); + RootedObject locationObj(cx); + RootedObject scope(cx, JS::CurrentGlobalOrNull(cx)); + rv = nsXPConnect::XPConnect()->WrapNative( + cx, scope, location, NS_GET_IID(nsIFile), locationObj.address()); + if (NS_SUCCEEDED(rv) && locationObj) { + args.rval().setObject(*locationObj); + } + } + } + + return true; +#endif +} + +static bool GetLine(JSContext* cx, char* bufp, FILE* file, const char* prompt) { + fputs(prompt, gOutFile); + fflush(gOutFile); + + char line[4096] = {'\0'}; + while (true) { + if (fgets(line, sizeof line, file)) { + strcpy(bufp, line); + return true; + } + if (errno != EINTR) { + return false; + } + } +} + +static bool ReadLine(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // While 4096 might be quite arbitrary, this is something to be fixed in + // bug 105707. It is also the same limit as in ProcessFile. + char buf[4096]; + RootedString str(cx); + + /* If a prompt was specified, construct the string */ + if (args.length() > 0) { + str = JS::ToString(cx, args[0]); + if (!str) { + return false; + } + } else { + str = JS_GetEmptyString(cx); + } + + /* Get a line from the infile */ + JS::UniqueChars strBytes = JS_EncodeStringToLatin1(cx, str); + if (!strBytes || !GetLine(cx, buf, gInFile, strBytes.get())) { + return false; + } + + /* Strip newline character added by GetLine() */ + unsigned int buflen = strlen(buf); + if (buflen == 0) { + if (feof(gInFile)) { + args.rval().setNull(); + return true; + } + } else if (buf[buflen - 1] == '\n') { + --buflen; + } + + /* Turn buf into a JSString */ + str = JS_NewStringCopyN(cx, buf, buflen); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +static bool Print(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setUndefined(); + +#ifdef FUZZING_INTERFACES + if (fuzzHaveModule && !fuzzDoDebug) { + // When fuzzing and not debugging, suppress any print() output, + // as it slows down fuzzing and makes libFuzzer's output hard + // to read. + return true; + } +#endif // FUZZING_INTERFACES + + RootedString str(cx); + nsAutoCString utf8output; + + for (unsigned i = 0; i < args.length(); i++) { + str = ToString(cx, args[i]); + if (!str) { + return false; + } + + JS::UniqueChars utf8str = JS_EncodeStringToUTF8(cx, str); + if (!utf8str) { + return false; + } + + if (i) { + utf8output.Append(' '); + } + utf8output.Append(utf8str.get(), strlen(utf8str.get())); + } + utf8output.Append('\n'); + fputs(utf8output.get(), gOutFile); + fflush(gOutFile); + return true; +} + +static bool Dump(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setUndefined(); + + if (!args.length()) { + return true; + } + + RootedString str(cx, ToString(cx, args[0])); + if (!str) { + return false; + } + + JS::UniqueChars utf8str = JS_EncodeStringToUTF8(cx, str); + if (!utf8str) { + return false; + } + +#ifdef ANDROID + __android_log_print(ANDROID_LOG_INFO, "Gecko", "%s", utf8str.get()); +#endif +#ifdef XP_WIN + if (IsDebuggerPresent()) { + nsAutoJSString wstr; + if (!wstr.init(cx, str)) { + return false; + } + OutputDebugStringW(wstr.get()); + } +#endif + fputs(utf8str.get(), gOutFile); + fflush(gOutFile); + return true; +} + +static bool Load(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + JS::RootedObject thisObject(cx); + if (!args.computeThis(cx, &thisObject)) { + return false; + } + if (!JS_IsGlobalObject(thisObject)) { + JS_ReportErrorASCII(cx, "Trying to load() into a non-global object"); + return false; + } + + RootedString str(cx); + for (unsigned i = 0; i < args.length(); i++) { + str = ToString(cx, args[i]); + if (!str) { + return false; + } + JS::UniqueChars filename = JS_EncodeStringToUTF8(cx, str); + if (!filename) { + return false; + } + JS::CompileOptions options(cx); + options.setIsRunOnce(true).setSkipFilenameValidation(true); + JS::Rooted<JSScript*> script( + cx, JS::CompileUtf8Path(cx, options, filename.get())); + if (!script) { + return false; + } + + if (!compileOnly) { + if (!JS_ExecuteScript(cx, script)) { + return false; + } + } + } + args.rval().setUndefined(); + return true; +} + +static bool Quit(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + gExitCode = 0; + if (!ToInt32(cx, args.get(0), &gExitCode)) { + return false; + } + + gQuitting = true; + // exit(0); + return false; +} + +static bool DumpXPC(JSContext* cx, unsigned argc, Value* vp) { + JS::CallArgs args = CallArgsFromVp(argc, vp); + + uint16_t depth = 2; + if (args.length() > 0) { + if (!JS::ToUint16(cx, args[0], &depth)) { + return false; + } + } + + nsXPConnect::XPConnect()->DebugDump(int16_t(depth)); + args.rval().setUndefined(); + return true; +} + +static bool GC(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + JS_GC(cx); + + args.rval().setUndefined(); + return true; +} + +#ifdef JS_GC_ZEAL +static bool GCZeal(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + uint32_t zeal; + if (!ToUint32(cx, args.get(0), &zeal)) { + return false; + } + + JS_SetGCZeal(cx, uint8_t(zeal), JS_DEFAULT_ZEAL_FREQ); + args.rval().setUndefined(); + return true; +} +#endif + +static bool SendCommand(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() == 0) { + JS_ReportErrorASCII(cx, "Function takes at least one argument!"); + return false; + } + + RootedString str(cx, ToString(cx, args[0])); + if (!str) { + JS_ReportErrorASCII(cx, "Could not convert argument 1 to string!"); + return false; + } + + if (args.get(1).isObject() && !JS_ObjectIsFunction(&args[1].toObject())) { + JS_ReportErrorASCII(cx, "Could not convert argument 2 to function!"); + return false; + } + + if (!XRE_SendTestShellCommand( + cx, str, args.length() > 1 ? args[1].address() : nullptr)) { + JS_ReportErrorASCII(cx, "Couldn't send command!"); + return false; + } + + args.rval().setUndefined(); + return true; +} + +static bool Options(JSContext* cx, unsigned argc, Value* vp) { + JS::CallArgs args = CallArgsFromVp(argc, vp); + ContextOptions oldContextOptions = ContextOptionsRef(cx); + + RootedString str(cx); + JS::UniqueChars opt; + for (unsigned i = 0; i < args.length(); ++i) { + str = ToString(cx, args[i]); + if (!str) { + return false; + } + + opt = JS_EncodeStringToUTF8(cx, str); + if (!opt) { + return false; + } + + if (strcmp(opt.get(), "strict_mode") == 0) { + ContextOptionsRef(cx).toggleStrictMode(); + } else { + JS_ReportErrorUTF8(cx, + "unknown option name '%s'. The valid name is " + "strict_mode.", + opt.get()); + return false; + } + } + + UniqueChars names; + if (names && oldContextOptions.strictMode()) { + names = JS_sprintf_append(std::move(names), "%s%s", names ? "," : "", + "strict_mode"); + if (!names) { + JS_ReportOutOfMemory(cx); + return false; + } + } + + str = JS_NewStringCopyZ(cx, names.get()); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +static PersistentRootedValue* sScriptedInterruptCallback = nullptr; + +static bool XPCShellInterruptCallback(JSContext* cx) { + MOZ_ASSERT(sScriptedInterruptCallback->initialized()); + RootedValue callback(cx, *sScriptedInterruptCallback); + + // If no interrupt callback was set by script, no-op. + if (callback.isUndefined()) { + return true; + } + + MOZ_ASSERT(js::IsFunctionObject(&callback.toObject())); + + JSAutoRealm ar(cx, &callback.toObject()); + RootedValue rv(cx); + if (!JS_CallFunctionValue(cx, nullptr, callback, + JS::HandleValueArray::empty(), &rv) || + !rv.isBoolean()) { + NS_WARNING("Scripted interrupt callback failed! Terminating script."); + JS_ClearPendingException(cx); + return false; + } + + return rv.toBoolean(); +} + +static bool SetInterruptCallback(JSContext* cx, unsigned argc, Value* vp) { + MOZ_ASSERT(sScriptedInterruptCallback->initialized()); + + // Sanity-check args. + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "Wrong number of arguments"); + return false; + } + + // Allow callers to remove the interrupt callback by passing undefined. + if (args[0].isUndefined()) { + *sScriptedInterruptCallback = UndefinedValue(); + return true; + } + + // Otherwise, we should have a function object. + if (!args[0].isObject() || !js::IsFunctionObject(&args[0].toObject())) { + JS_ReportErrorASCII(cx, "Argument must be a function"); + return false; + } + + *sScriptedInterruptCallback = args[0]; + + return true; +} + +static bool SimulateNoScriptActivity(JSContext* cx, unsigned argc, Value* vp) { + // Sanity-check args. + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + if (args.length() != 1 || !args[0].isInt32() || args[0].toInt32() < 0) { + JS_ReportErrorASCII(cx, "Expected a positive integer argument"); + return false; + } + + // This mimics mozilla::SpinEventLoopUntil but instead of spinning the + // event loop we sleep, to make sure we don't run script. + xpc::AutoScriptActivity asa(false); + PR_Sleep(PR_SecondsToInterval(args[0].toInt32())); + + args.rval().setUndefined(); + return true; +} + +static bool RegisterAppManifest(JSContext* cx, unsigned argc, Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "Wrong number of arguments"); + return false; + } + if (!args[0].isObject()) { + JS_ReportErrorASCII(cx, + "Expected object as argument 1 to registerAppManifest"); + return false; + } + + Rooted<JSObject*> arg1(cx, &args[0].toObject()); + nsCOMPtr<nsIFile> file; + nsresult rv = nsXPConnect::XPConnect()->WrapJS(cx, arg1, NS_GET_IID(nsIFile), + getter_AddRefs(file)); + if (NS_FAILED(rv)) { + XPCThrower::Throw(rv, cx); + return false; + } + rv = XRE_AddManifestLocation(NS_APP_LOCATION, file); + if (NS_FAILED(rv)) { + XPCThrower::Throw(rv, cx); + return false; + } + return true; +} + +#ifdef ANDROID +static bool ChangeTestShellDir(JSContext* cx, unsigned argc, Value* vp) { + // This method should only be used by testing/xpcshell/head.js to change to + // the correct directory on Android Remote XPCShell tests. + // + // TODO: Bug 1801725 - Find a more ergonomic way to do this than exposing + // identical methods in XPCShellEnvironment and XPCShellImpl to chdir on + // android for Remote XPCShell tests on Android. + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "changeTestShellDir() takes one argument"); + return false; + } + + nsAutoJSCString path; + if (!path.init(cx, args[0])) { + JS_ReportErrorASCII( + cx, "changeTestShellDir(): could not convert argument 1 to string"); + return false; + } + + if (chdir(path.get())) { + JS_ReportErrorASCII(cx, "changeTestShellDir(): could not change directory"); + return false; + } + + return true; +} +#endif + +#ifdef ENABLE_TESTS +static bool RegisterXPCTestComponents(JSContext* cx, unsigned argc, Value* vp) { + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); + if (args.length() != 0) { + JS_ReportErrorASCII(cx, "Wrong number of arguments"); + return false; + } + nsresult rv = xpcTestRegisterComponents(); + if (NS_FAILED(rv)) { + XPCThrower::Throw(rv, cx); + return false; + } + return true; +} +#endif + +static const JSFunctionSpec glob_functions[] = { + // clang-format off + JS_FN("print", Print, 0,0), + JS_FN("readline", ReadLine, 1,0), + JS_FN("load", Load, 1,0), + JS_FN("quit", Quit, 0,0), + JS_FN("dumpXPC", DumpXPC, 1,0), + JS_FN("dump", Dump, 1,0), + JS_FN("gc", GC, 0,0), +#ifdef JS_GC_ZEAL + JS_FN("gczeal", GCZeal, 1,0), +#endif + JS_FN("options", Options, 0,0), + JS_FN("sendCommand", SendCommand, 1,0), + JS_FN("atob", xpc::Atob, 1,0), + JS_FN("btoa", xpc::Btoa, 1,0), + JS_FN("setInterruptCallback", SetInterruptCallback, 1,0), + JS_FN("simulateNoScriptActivity", SimulateNoScriptActivity, 1,0), + JS_FN("registerAppManifest", RegisterAppManifest, 1, 0), +#ifdef ANDROID + JS_FN("changeTestShellDir", ChangeTestShellDir, 1,0), +#endif +#ifdef ENABLE_TESTS + JS_FN("registerXPCTestComponents", RegisterXPCTestComponents, 0, 0), +#endif + JS_FS_END + // clang-format on +}; + +/***************************************************************************/ + +typedef enum JSShellErrNum { +#define MSG_DEF(name, number, count, exception, format) name = number, +#include "jsshell.msg" +#undef MSG_DEF + JSShellErr_Limit +} JSShellErrNum; + +static const JSErrorFormatString jsShell_ErrorFormatString[JSShellErr_Limit] = { +#define MSG_DEF(name, number, count, exception, format) {#name, format, count}, +#include "jsshell.msg" +#undef MSG_DEF +}; + +static const JSErrorFormatString* my_GetErrorMessage( + void* userRef, const unsigned errorNumber) { + if (errorNumber == 0 || errorNumber >= JSShellErr_Limit) { + return nullptr; + } + + return &jsShell_ErrorFormatString[errorNumber]; +} + +static bool ProcessUtf8Line(AutoJSAPI& jsapi, const char* buffer, + int startline) { + JSContext* cx = jsapi.cx(); + JS::CompileOptions options(cx); + options.setFileAndLine("typein", startline) + .setIsRunOnce(true) + .setSkipFilenameValidation(true); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + if (!srcBuf.init(cx, buffer, strlen(buffer), JS::SourceOwnership::Borrowed)) { + return false; + } + + JS::RootedScript script(cx, JS::Compile(cx, options, srcBuf)); + if (!script) { + return false; + } + if (compileOnly) { + return true; + } + + JS::RootedValue result(cx); + if (!JS_ExecuteScript(cx, script, &result)) { + return false; + } + + if (result.isUndefined()) { + return true; + } + + RootedString str(cx, JS::ToString(cx, result)); + if (!str) { + return false; + } + + JS::UniqueChars bytes = JS_EncodeStringToLatin1(cx, str); + if (!bytes) { + return false; + } + + fprintf(gOutFile, "%s\n", bytes.get()); + return true; +} + +static bool ProcessFile(AutoJSAPI& jsapi, const char* filename, FILE* file, + bool forceTTY) { + JSContext* cx = jsapi.cx(); + JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx)); + MOZ_ASSERT(global); + + if (forceTTY) { + file = stdin; + } else if (!isatty(fileno(file))) { + /* + * It's not interactive - just execute it. + * + * Support the UNIX #! shell hack; gobble the first line if it starts + * with '#'. + */ + int ch = fgetc(file); + if (ch == '#') { + while ((ch = fgetc(file)) != EOF) { + if (ch == '\n' || ch == '\r') { + break; + } + } + } + ungetc(ch, file); + + JS::UniqueChars filenameUtf8 = JS::EncodeNarrowToUtf8(jsapi.cx(), filename); + if (!filenameUtf8) { + return false; + } + + JS::RootedScript script(cx); + JS::RootedValue unused(cx); + JS::CompileOptions options(cx); + options.setFileAndLine(filenameUtf8.get(), 1) + .setIsRunOnce(true) + .setNoScriptRval(true) + .setSkipFilenameValidation(true); + script = JS::CompileUtf8File(cx, options, file); + if (!script) { + return false; + } + return compileOnly || JS_ExecuteScript(cx, script, &unused); + } + + /* It's an interactive filehandle; drop into read-eval-print loop. */ + int lineno = 1; + bool hitEOF = false; + do { + char buffer[4096]; + char* bufp = buffer; + *bufp = '\0'; + + /* + * Accumulate lines until we get a 'compilable unit' - one that either + * generates an error (before running out of source) or that compiles + * cleanly. This should be whenever we get a complete statement that + * coincides with the end of a line. + */ + int startline = lineno; + do { + if (!GetLine(cx, bufp, file, startline == lineno ? "js> " : "")) { + hitEOF = true; + break; + } + bufp += strlen(bufp); + lineno++; + } while ( + !JS_Utf8BufferIsCompilableUnit(cx, global, buffer, strlen(buffer))); + + if (!ProcessUtf8Line(jsapi, buffer, startline)) { + jsapi.ReportException(); + } + } while (!hitEOF && !gQuitting); + + fprintf(gOutFile, "\n"); + return true; +} + +static bool Process(AutoJSAPI& jsapi, const char* filename, bool forceTTY) { + FILE* file; + + if (forceTTY || !filename || strcmp(filename, "-") == 0) { + file = stdin; + } else { + file = fopen(filename, "r"); + if (!file) { + /* + * Use Latin1 variant here because the encoding of the return value + * of strerror function can be non-UTF-8. + */ + JS_ReportErrorNumberLatin1(jsapi.cx(), my_GetErrorMessage, nullptr, + JSSMSG_CANT_OPEN, filename, strerror(errno)); + gExitCode = EXITCODE_FILE_NOT_FOUND; + return false; + } + } + + bool ok = ProcessFile(jsapi, filename, file, forceTTY); + if (file != stdin) { + fclose(file); + } + return ok; +} + +static int usage() { + fprintf(gErrFile, "%s\n", JS_GetImplementationVersion()); + fprintf( + gErrFile, + "usage: xpcshell [-g gredir] [-a appdir] [-r manifest]... [-WwxiCmIp] " + "[-f scriptfile] [-e script] [scriptfile] [scriptarg...]\n"); + return 2; +} + +static bool printUsageAndSetExitCode() { + gExitCode = usage(); + return false; +} + +static bool ProcessArgs(AutoJSAPI& jsapi, char** argv, int argc, + XPCShellDirProvider* aDirProvider) { + JSContext* cx = jsapi.cx(); + const char rcfilename[] = "xpcshell.js"; + FILE* rcfile; + int rootPosition; + JS::Rooted<JSObject*> argsObj(cx); + char* filename = nullptr; + bool isInteractive = true; + bool forceTTY = false; + + rcfile = fopen(rcfilename, "r"); + if (rcfile) { + printf("[loading '%s'...]\n", rcfilename); + bool ok = ProcessFile(jsapi, rcfilename, rcfile, false); + fclose(rcfile); + if (!ok) { + return false; + } + } + + JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx)); + + /* + * Scan past all optional arguments so we can create the arguments object + * before processing any -f options, which must interleave properly with + * -v and -w options. This requires two passes, and without getopt, we'll + * have to keep the option logic here and in the second for loop in sync. + * First of all, find out the first argument position which will be passed + * as a script file to be executed. + */ + for (rootPosition = 0; rootPosition < argc; rootPosition++) { + if (argv[rootPosition][0] != '-' || argv[rootPosition][1] == '\0') { + ++rootPosition; + break; + } + + bool isPairedFlag = + argv[rootPosition][0] != '\0' && + (argv[rootPosition][1] == 'v' || argv[rootPosition][1] == 'f' || + argv[rootPosition][1] == 'e'); + if (isPairedFlag && rootPosition < argc - 1) { + ++rootPosition; // Skip over the 'foo' portion of |-v foo|, |-f foo|, or + // |-e foo|. + } + } + + /* + * Create arguments early and define it to root it, so it's safe from any + * GC calls nested below, and so it is available to -f <file> arguments. + */ + argsObj = JS::NewArrayObject(cx, 0); + if (!argsObj) { + return 1; + } + if (!JS_DefineProperty(cx, global, "arguments", argsObj, 0)) { + return 1; + } + + for (int j = 0, length = argc - rootPosition; j < length; j++) { + RootedString str(cx, JS_NewStringCopyZ(cx, argv[rootPosition++])); + if (!str || !JS_DefineElement(cx, argsObj, j, str, JSPROP_ENUMERATE)) { + return 1; + } + } + + for (int i = 0; i < argc; i++) { + if (argv[i][0] != '-' || argv[i][1] == '\0') { + filename = argv[i++]; + isInteractive = false; + break; + } + switch (argv[i][1]) { + case 'W': + reportWarnings = false; + break; + case 'w': + reportWarnings = true; + break; + case 'x': + break; + case 'd': + /* This used to try to turn on the debugger. */ + break; + case 'm': + break; + case 'f': + if (++i == argc) { + return printUsageAndSetExitCode(); + } + if (!Process(jsapi, argv[i], false)) { + return false; + } + /* + * XXX: js -f foo.js should interpret foo.js and then + * drop into interactive mode, but that breaks test + * harness. Just execute foo.js for now. + */ + isInteractive = false; + break; + case 'i': + isInteractive = forceTTY = true; + break; + case 'e': { + RootedValue rval(cx); + + if (++i == argc) { + return printUsageAndSetExitCode(); + } + + JS::CompileOptions opts(cx); + opts.setSkipFilenameValidation(true); + opts.setFileAndLine("-e", 1); + + JS::SourceText<mozilla::Utf8Unit> srcBuf; + if (srcBuf.init(cx, argv[i], strlen(argv[i]), + JS::SourceOwnership::Borrowed)) { + JS::Evaluate(cx, opts, srcBuf, &rval); + } + + isInteractive = false; + break; + } + case 'C': + compileOnly = true; + isInteractive = false; + break; + default: + return printUsageAndSetExitCode(); + } + } + + if (filename || isInteractive) { + return Process(jsapi, filename, forceTTY); + } + return true; +} + +/***************************************************************************/ + +static bool GetCurrentWorkingDirectory(nsAString& workingDirectory) { +#if !defined(XP_WIN) && !defined(XP_UNIX) + // XXX: your platform should really implement this + return false; +#elif XP_WIN + DWORD requiredLength = GetCurrentDirectoryW(0, nullptr); + workingDirectory.SetLength(requiredLength); + GetCurrentDirectoryW(workingDirectory.Length(), + (LPWSTR)workingDirectory.BeginWriting()); + // we got a trailing null there + workingDirectory.SetLength(requiredLength); + workingDirectory.Replace(workingDirectory.Length() - 1, 1, L'\\'); +#elif defined(XP_UNIX) + nsAutoCString cwd; + // 1024 is just a guess at a sane starting value + size_t bufsize = 1024; + char* result = nullptr; + while (result == nullptr) { + cwd.SetLength(bufsize); + result = getcwd(cwd.BeginWriting(), cwd.Length()); + if (!result) { + if (errno != ERANGE) { + return false; + } + // need to make the buffer bigger + bufsize *= 2; + } + } + // size back down to the actual string length + cwd.SetLength(strlen(result) + 1); + cwd.Replace(cwd.Length() - 1, 1, '/'); + CopyUTF8toUTF16(cwd, workingDirectory); +#endif + return true; +} + +static JSSecurityCallbacks shellSecurityCallbacks; + +int XRE_XPCShellMain(int argc, char** argv, char** envp, + const XREShellData* aShellData) { + MOZ_ASSERT(aShellData); + + JSContext* cx; + int result = 0; + nsresult rv; + +#ifdef ANDROID + gOutFile = aShellData->outFile; + gErrFile = aShellData->errFile; +#else + gOutFile = stdout; + gErrFile = stderr; +#endif + gInFile = stdin; + + NS_LogInit(); + + mozilla::LogModule::Init(argc, argv); + + // This guard ensures that all threads that attempt to register themselves + // with the IOInterposer will be properly tracked. + mozilla::IOInterposerInit ioInterposerGuard; + + XRE_InitCommandLine(argc, argv); + + char aLocal; + profiler_init(&aLocal); + +#ifdef MOZ_ASAN_REPORTER + PR_SetEnv("MOZ_DISABLE_ASAN_REPORTER=1"); +#endif + + if (PR_GetEnv("MOZ_CHAOSMODE")) { + ChaosFeature feature = ChaosFeature::Any; + long featureInt = strtol(PR_GetEnv("MOZ_CHAOSMODE"), nullptr, 16); + if (featureInt) { + // NOTE: MOZ_CHAOSMODE=0 or a non-hex value maps to Any feature. + feature = static_cast<ChaosFeature>(featureInt); + } + ChaosMode::SetChaosFeature(feature); + } + + if (ChaosMode::isActive(ChaosFeature::Any)) { + printf_stderr( + "*** You are running in chaos test mode. See ChaosMode.h. ***\n"); + } + +#ifdef XP_WIN + // Some COM settings are global to the process and must be set before any non- + // trivial COM is run in the application. Since these settings may affect + // stability, we should instantiate COM ASAP so that we can ensure that these + // global settings are configured before anything can interfere. + mscom::ProcessRuntime mscom; +#endif + + // The provider needs to outlive the call to shutting down XPCOM. + XPCShellDirProvider dirprovider; + + { // Start scoping nsCOMPtrs + nsCOMPtr<nsIFile> appFile; + rv = XRE_GetBinaryPath(getter_AddRefs(appFile)); + if (NS_FAILED(rv)) { + printf("Couldn't find application file.\n"); + return 1; + } + nsCOMPtr<nsIFile> appDir; + rv = appFile->GetParent(getter_AddRefs(appDir)); + if (NS_FAILED(rv)) { + printf("Couldn't get application directory.\n"); + return 1; + } + + dirprovider.SetAppFile(appFile); + + nsCOMPtr<nsIFile> greDir; + if (argc > 1 && !strcmp(argv[1], "-g")) { + if (argc < 3) { + return usage(); + } + + rv = XRE_GetFileFromPath(argv[2], getter_AddRefs(greDir)); + if (NS_FAILED(rv)) { + printf("Couldn't use given GRE dir.\n"); + return 1; + } + + dirprovider.SetGREDirs(greDir); + + argc -= 2; + argv += 2; + } else { +#ifdef XP_MACOSX + // On OSX, the GreD needs to point to Contents/Resources in the .app + // bundle. Libraries will be loaded at a relative path to GreD, i.e. + // ../MacOS. + nsCOMPtr<nsIFile> tmpDir; + XRE_GetFileFromPath(argv[0], getter_AddRefs(greDir)); + greDir->GetParent(getter_AddRefs(tmpDir)); + tmpDir->Clone(getter_AddRefs(greDir)); + tmpDir->SetNativeLeafName("Resources"_ns); + bool dirExists = false; + tmpDir->Exists(&dirExists); + if (dirExists) { + greDir = tmpDir.forget(); + } + dirprovider.SetGREDirs(greDir); +#else + nsAutoString workingDir; + if (!GetCurrentWorkingDirectory(workingDir)) { + printf("GetCurrentWorkingDirectory failed.\n"); + return 1; + } + rv = NS_NewLocalFile(workingDir, true, getter_AddRefs(greDir)); + if (NS_FAILED(rv)) { + printf("NS_NewLocalFile failed.\n"); + return 1; + } +#endif + } + + if (argc > 1 && !strcmp(argv[1], "-a")) { + if (argc < 3) { + return usage(); + } + + nsCOMPtr<nsIFile> dir; + rv = XRE_GetFileFromPath(argv[2], getter_AddRefs(dir)); + if (NS_SUCCEEDED(rv)) { + appDir = dir; + dirprovider.SetAppDir(appDir); + } + if (NS_FAILED(rv)) { + printf("Couldn't use given appdir.\n"); + return 1; + } + argc -= 2; + argv += 2; + } + + while (argc > 1 && !strcmp(argv[1], "-r")) { + if (argc < 3) { + return usage(); + } + + nsCOMPtr<nsIFile> lf; + rv = XRE_GetFileFromPath(argv[2], getter_AddRefs(lf)); + if (NS_FAILED(rv)) { + printf("Couldn't get manifest file.\n"); + return 1; + } + XRE_AddManifestLocation(NS_APP_LOCATION, lf); + + argc -= 2; + argv += 2; + } + + const char* val = getenv("MOZ_CRASHREPORTER"); + if (val && *val && !CrashReporter::IsDummy()) { + rv = CrashReporter::SetExceptionHandler(greDir, true); + if (NS_FAILED(rv)) { + printf("CrashReporter::SetExceptionHandler failed!\n"); + return 1; + } + MOZ_ASSERT(CrashReporter::GetEnabled()); + } + + if (argc > 1 && !strcmp(argv[1], "--greomni")) { + nsCOMPtr<nsIFile> greOmni; + XRE_GetFileFromPath(argv[2], getter_AddRefs(greOmni)); + XRE_InitOmnijar(greOmni, greOmni); + argc -= 2; + argv += 2; + } + + rv = NS_InitXPCOM(nullptr, appDir, &dirprovider); + if (NS_FAILED(rv)) { + printf("NS_InitXPCOM failed!\n"); + return 1; + } + + // xpc::ErrorReport::LogToConsoleWithStack needs this to print errors + // to stderr. + Preferences::SetBool("browser.dom.window.dump.enabled", true); + Preferences::SetBool("devtools.console.stdout.chrome", true); + + AutoJSAPI jsapi; + jsapi.Init(); + cx = jsapi.cx(); + + // Override the default XPConnect interrupt callback. We could store the + // old one and restore it before shutting down, but there's not really a + // reason to bother. + sScriptedInterruptCallback = new PersistentRootedValue; + sScriptedInterruptCallback->init(cx, UndefinedValue()); + + JS_AddInterruptCallback(cx, XPCShellInterruptCallback); + + argc--; + argv++; + + nsCOMPtr<nsIPrincipal> systemprincipal; + // Fetch the system principal and store it away in a global, to use for + // script compilation in Load() and ProcessFile() (including interactive + // eval loop) + { + nsCOMPtr<nsIScriptSecurityManager> securityManager = + do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &rv); + if (NS_SUCCEEDED(rv) && securityManager) { + rv = securityManager->GetSystemPrincipal( + getter_AddRefs(systemprincipal)); + if (NS_FAILED(rv)) { + fprintf(gErrFile, + "+++ Failed to obtain SystemPrincipal from " + "ScriptSecurityManager service.\n"); + } else { + // fetch the JS principals and stick in a global + gJSPrincipals = nsJSPrincipals::get(systemprincipal); + JS_HoldPrincipals(gJSPrincipals); + } + } else { + fprintf(gErrFile, + "+++ Failed to get ScriptSecurityManager service, running " + "without principals"); + } + } + + const JSSecurityCallbacks* scb = JS_GetSecurityCallbacks(cx); + MOZ_ASSERT( + scb, + "We are assuming that nsScriptSecurityManager::Init() has been run"); + shellSecurityCallbacks = *scb; + JS_SetSecurityCallbacks(cx, &shellSecurityCallbacks); + + auto backstagePass = MakeRefPtr<BackstagePass>(); + + // Make the default XPCShell global use a fresh zone (rather than the + // System Zone) to improve cross-zone test coverage. + JS::RealmOptions options; + options.creationOptions().setNewCompartmentAndZone(); + xpc::SetPrefableRealmOptions(options); + + // Even if we're building in a configuration where source is + // discarded, there's no reason to do that on XPCShell, and doing so + // might break various automation scripts. + options.behaviors().setDiscardSource(false); + + JS::Rooted<JSObject*> glob(cx); + rv = xpc::InitClassesWithNewWrappedGlobal( + cx, static_cast<nsIGlobalObject*>(backstagePass), systemprincipal, 0, + options, &glob); + if (NS_FAILED(rv)) { + return 1; + } + + // Initialize e10s check on the main thread, if not already done + BrowserTabsRemoteAutostart(); +#if defined(XP_WIN) + // Plugin may require audio session if installed plugin can initialize + // asynchronized. + AutoAudioSession audioSession; + + // Ensure that DLL Services are running + RefPtr<DllServices> dllSvc(DllServices::Get()); + dllSvc->StartUntrustedModulesProcessor(true); + auto dllServicesDisable = + MakeScopeExit([&dllSvc]() { dllSvc->DisableFull(); }); + +# if defined(MOZ_SANDBOX) + // Required for sandboxed child processes. + if (aShellData->sandboxBrokerServices) { + SandboxBroker::Initialize(aShellData->sandboxBrokerServices); + SandboxBroker::GeckoDependentInitialize(); + } else { + NS_WARNING( + "Failed to initialize broker services, sandboxed " + "processes will fail to start."); + } +# endif // defined(MOZ_SANDBOX) + + { + DebugOnly<bool> result = WindowsBCryptInitialization(); + MOZ_ASSERT(result); + } +#endif // defined(XP_WIN) + +#ifdef MOZ_CODE_COVERAGE + CodeCoverageHandler::Init(); +#endif + + { + if (!glob) { + return 1; + } + + nsCOMPtr<nsIAppStartup> appStartup(components::AppStartup::Service()); + if (!appStartup) { + return 1; + } + appStartup->DoneStartingUp(); + + backstagePass->SetGlobalObject(glob); + + JSAutoRealm ar(cx, glob); + + if (!JS_InitReflectParse(cx, glob)) { + return 1; + } + + if (!JS_DefineFunctions(cx, glob, glob_functions)) { + return 1; + } + + nsAutoString workingDirectory; + if (GetCurrentWorkingDirectory(workingDirectory)) { + gWorkingDirectory = &workingDirectory; + } + + JS_DefineProperty(cx, glob, "__LOCATION__", GetLocationProperty, nullptr, + 0); + + { +#ifdef FUZZING_INTERFACES + if (fuzzHaveModule) { +# ifdef LIBFUZZER + // argv[0] was removed previously, but libFuzzer expects it + argc++; + argv--; + + result = FuzzXPCRuntimeStart(&jsapi, &argc, &argv, + aShellData->fuzzerDriver); +# elif AFLFUZZ + MOZ_CRASH("AFL is unsupported for XPC runtime fuzzing integration"); +# endif + } else { +#endif + // We are almost certainly going to run script here, so we need an + // AutoEntryScript. This is Gecko-specific and not in any spec. + AutoEntryScript aes(backstagePass, "xpcshell argument processing"); + + // If an exception is thrown, we'll set our return code + // appropriately, and then let the AutoEntryScript destructor report + // the error to the console. + if (!ProcessArgs(aes, argv, argc, &dirprovider)) { + if (gExitCode) { + result = gExitCode; + } else if (gQuitting) { + result = 0; + } else { + result = EXITCODE_RUNTIME_ERROR; + } + } +#ifdef FUZZING_INTERFACES + } +#endif + } + + // Signal that we're now shutting down. + nsCOMPtr<nsIObserver> obs = do_QueryInterface(appStartup); + if (obs) { + obs->Observe(nullptr, "quit-application-forced", nullptr); + } + + JS_DropPrincipals(cx, gJSPrincipals); + JS_SetAllNonReservedSlotsToUndefined(glob); + JS::RootedObject lexicalEnv(cx, JS_GlobalLexicalEnvironment(glob)); + JS_SetAllNonReservedSlotsToUndefined(lexicalEnv); + JS_GC(cx); + } + JS_GC(cx); + + dirprovider.ClearGREDirs(); + dirprovider.ClearAppDir(); + dirprovider.ClearAppFile(); + } // this scopes the nsCOMPtrs + + if (!XRE_ShutdownTestShell()) { + NS_ERROR("problem shutting down testshell"); + } + + // no nsCOMPtrs are allowed to be alive when you call NS_ShutdownXPCOM + rv = NS_ShutdownXPCOM(nullptr); + MOZ_ASSERT(NS_SUCCEEDED(rv), "NS_ShutdownXPCOM failed"); + + // Shut down the crashreporter service to prevent leaking some strings it + // holds. + if (CrashReporter::GetEnabled()) { + CrashReporter::UnsetExceptionHandler(); + } + + // This must precede NS_LogTerm(), otherwise xpcshell return non-zero + // during some tests, which causes failures. + profiler_shutdown(); + + NS_LogTerm(); + + XRE_DeinitCommandLine(); + + return result; +} + +void XPCShellDirProvider::SetGREDirs(nsIFile* greDir) { + mGREDir = greDir; + mGREDir->Clone(getter_AddRefs(mGREBinDir)); + +#ifdef XP_MACOSX + nsAutoCString leafName; + mGREDir->GetNativeLeafName(leafName); + if (leafName.EqualsLiteral("Resources")) { + mGREBinDir->SetNativeLeafName("MacOS"_ns); + } +#endif +} + +void XPCShellDirProvider::SetAppFile(nsIFile* appFile) { mAppFile = appFile; } + +void XPCShellDirProvider::SetAppDir(nsIFile* appDir) { mAppDir = appDir; } + +NS_IMETHODIMP_(MozExternalRefCountType) +XPCShellDirProvider::AddRef() { return 2; } + +NS_IMETHODIMP_(MozExternalRefCountType) +XPCShellDirProvider::Release() { return 1; } + +NS_IMPL_QUERY_INTERFACE(XPCShellDirProvider, nsIDirectoryServiceProvider, + nsIDirectoryServiceProvider2) + +NS_IMETHODIMP +XPCShellDirProvider::GetFile(const char* prop, bool* persistent, + nsIFile** result) { + if (mGREDir && !strcmp(prop, NS_GRE_DIR)) { + *persistent = true; + return mGREDir->Clone(result); + } else if (mGREBinDir && !strcmp(prop, NS_GRE_BIN_DIR)) { + *persistent = true; + return mGREBinDir->Clone(result); + } else if (mAppFile && !strcmp(prop, XRE_EXECUTABLE_FILE)) { + *persistent = true; + return mAppFile->Clone(result); + } else if (mGREDir && !strcmp(prop, NS_APP_PREF_DEFAULTS_50_DIR)) { + nsCOMPtr<nsIFile> file; + *persistent = true; + if (NS_FAILED(mGREDir->Clone(getter_AddRefs(file))) || + NS_FAILED(file->AppendNative("defaults"_ns)) || + NS_FAILED(file->AppendNative("pref"_ns))) + return NS_ERROR_FAILURE; + file.forget(result); + return NS_OK; + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +XPCShellDirProvider::GetFiles(const char* prop, nsISimpleEnumerator** result) { + if (mGREDir && !strcmp(prop, "ChromeML")) { + nsCOMArray<nsIFile> dirs; + + nsCOMPtr<nsIFile> file; + mGREDir->Clone(getter_AddRefs(file)); + file->AppendNative("chrome"_ns); + dirs.AppendObject(file); + + nsresult rv = + NS_GetSpecialDirectory(NS_APP_CHROME_DIR, getter_AddRefs(file)); + if (NS_SUCCEEDED(rv)) { + dirs.AppendObject(file); + } + + return NS_NewArrayEnumerator(result, dirs, NS_GET_IID(nsIFile)); + } else if (!strcmp(prop, NS_APP_PREFS_DEFAULTS_DIR_LIST)) { + nsCOMArray<nsIFile> dirs; + nsCOMPtr<nsIFile> appDir; + bool exists; + if (mAppDir && NS_SUCCEEDED(mAppDir->Clone(getter_AddRefs(appDir))) && + NS_SUCCEEDED(appDir->AppendNative("defaults"_ns)) && + NS_SUCCEEDED(appDir->AppendNative("preferences"_ns)) && + NS_SUCCEEDED(appDir->Exists(&exists)) && exists) { + dirs.AppendObject(appDir); + return NS_NewArrayEnumerator(result, dirs, NS_GET_IID(nsIFile)); + } + return NS_ERROR_FAILURE; + } + return NS_ERROR_FAILURE; +} |