From e0e83068e7c8e7b11667715ae90a6cec48cff923 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Wed, 11 Nov 2026 14:17:09 -0300 Subject: [PATCH 1/8] refactor: single app-root accessor Replace Constants::APP_ROOT_FOLDER_PATH and ModuleInternalCallbacks' JNI-cached GetApplicationPath() with tns::AppFiles, initialized once from Runtime::Init. AppRoot() is "/app" and AppRootPrefix() is "/app/", matching the two slash conventions the call sites used, so SBG extend naming, script origins and worker caller-dir derivation produce the same strings as before. The "~/" require path no longer produces "/app//tail". --- test-app/runtime/CMakeLists.txt | 1 + test-app/runtime/src/main/cpp/AppFiles.cpp | 36 +++++++++++++++++++ test-app/runtime/src/main/cpp/AppFiles.h | 24 +++++++++++++ .../runtime/src/main/cpp/CallbackHandlers.cpp | 11 +++--- test-app/runtime/src/main/cpp/Constants.cpp | 1 - test-app/runtime/src/main/cpp/Constants.h | 1 - .../src/main/cpp/JsV8InspectorClient.cpp | 5 +-- .../runtime/src/main/cpp/MetadataNode.cpp | 9 ++--- .../runtime/src/main/cpp/ModuleInternal.cpp | 8 ++--- .../runtime/src/main/cpp/ModuleInternal.h | 1 - .../src/main/cpp/ModuleInternalCallbacks.cpp | 24 ++----------- test-app/runtime/src/main/cpp/Runtime.cpp | 3 +- 12 files changed, 83 insertions(+), 41 deletions(-) create mode 100644 test-app/runtime/src/main/cpp/AppFiles.cpp create mode 100644 test-app/runtime/src/main/cpp/AppFiles.h diff --git a/test-app/runtime/CMakeLists.txt b/test-app/runtime/CMakeLists.txt index fb87868c4..5f250d7fb 100644 --- a/test-app/runtime/CMakeLists.txt +++ b/test-app/runtime/CMakeLists.txt @@ -174,6 +174,7 @@ add_library( SHARED # Runtime source + src/main/cpp/AppFiles.cpp src/main/cpp/ArgConverter.cpp src/main/cpp/ArrayBufferHelper.cpp src/main/cpp/ArrayElementAccessor.cpp diff --git a/test-app/runtime/src/main/cpp/AppFiles.cpp b/test-app/runtime/src/main/cpp/AppFiles.cpp new file mode 100644 index 000000000..22498f83f --- /dev/null +++ b/test-app/runtime/src/main/cpp/AppFiles.cpp @@ -0,0 +1,36 @@ +#include "AppFiles.h" + +#include + +namespace tns { +namespace AppFiles { + +namespace { + +struct Roots { + std::string app; + std::string appPrefix; +}; + +Roots s_roots; +std::once_flag s_initOnce; + +} // namespace + +void Init(const std::string& filesRoot) { + std::call_once(s_initOnce, [&filesRoot]() { + s_roots.app = filesRoot + "/app"; + s_roots.appPrefix = s_roots.app + "/"; + }); +} + +const std::string& AppRoot() { + return s_roots.app; +} + +const std::string& AppRootPrefix() { + return s_roots.appPrefix; +} + +} // namespace AppFiles +} // namespace tns diff --git a/test-app/runtime/src/main/cpp/AppFiles.h b/test-app/runtime/src/main/cpp/AppFiles.h new file mode 100644 index 000000000..77bdef2ec --- /dev/null +++ b/test-app/runtime/src/main/cpp/AppFiles.h @@ -0,0 +1,24 @@ +#ifndef APPFILES_H_ +#define APPFILES_H_ + +#include + +namespace tns { +namespace AppFiles { + +/* + * Sets the app's files directory. Only the first call takes effect, so every + * runtime may call it; the roots below are empty until then. + */ +void Init(const std::string& filesRoot); + +// "/app", no trailing slash. +const std::string& AppRoot(); + +// "/app/", for prefix checks and joining relative names. +const std::string& AppRootPrefix(); + +} // namespace AppFiles +} // namespace tns + +#endif /* APPFILES_H_ */ diff --git a/test-app/runtime/src/main/cpp/CallbackHandlers.cpp b/test-app/runtime/src/main/cpp/CallbackHandlers.cpp index c4d1e7a2f..886b5da0f 100644 --- a/test-app/runtime/src/main/cpp/CallbackHandlers.cpp +++ b/test-app/runtime/src/main/cpp/CallbackHandlers.cpp @@ -1,4 +1,5 @@ #include "CallbackHandlers.h" +#include "AppFiles.h" #include "NativeScriptAssert.h" #include "MetadataNode.h" #include "Util.h" @@ -1514,7 +1515,7 @@ void CallbackHandlers::NewThreadCallback(const v8::FunctionCallbackInfo= 2 && workerPath[1] == '/' ? workerPath.substr(2) : workerPath.substr(1); - resolvedPath = Constants::APP_ROOT_FOLDER_PATH + tail; + resolvedPath = AppFiles::AppRootPrefix() + tail; } /* @@ -1523,7 +1524,7 @@ void CallbackHandlers::NewThreadCallback(const v8::FunctionCallbackInfo() : StackTrace::CurrentStackTrace(isolate, 1, StackTrace::kScriptName); @@ -1558,13 +1559,13 @@ void CallbackHandlers::NewThreadCallback(const v8::FunctionCallbackInfo #include #include @@ -196,7 +197,7 @@ bool GetInt32(const json& value, long long& out) { bool ShouldRewriteSourceMapURLs() { static bool disabled = []() { int length = 0; - auto* data = static_cast(File::ReadBinary(Constants::APP_ROOT_FOLDER_PATH + "package.json", length)); + auto* data = static_cast(File::ReadBinary(AppFiles::AppRootPrefix() + "package.json", length)); if (data == nullptr) { return false; } @@ -837,7 +838,7 @@ std::string JsV8InspectorClient::HandleLoadNetworkResource(long long msgId, cons auto addVariants = [&addCandidate](const std::string& p) { addCandidate(p); if (p[0] != '/') { - addCandidate(Constants::APP_ROOT_FOLDER_PATH + p); + addCandidate(AppFiles::AppRootPrefix() + p); } // /data/data/ and /data/user/0/ alias the same files; // urls may use either spelling depending on who built them. diff --git a/test-app/runtime/src/main/cpp/MetadataNode.cpp b/test-app/runtime/src/main/cpp/MetadataNode.cpp index adb1b3ebe..70995b419 100644 --- a/test-app/runtime/src/main/cpp/MetadataNode.cpp +++ b/test-app/runtime/src/main/cpp/MetadataNode.cpp @@ -1,4 +1,5 @@ #include "MetadataNode.h" +#include "AppFiles.h" #include "RuntimeState.h" #include "NativeScriptAssert.h" #include "Constants.h" @@ -906,7 +907,7 @@ vector MetadataNode::SetInstanceMethodsFromS Local funcData = External::New(isolate, callbackData, v8::kExternalPointerTypeTagDefault); Local funcTemplate = NewNonConstructor(isolate, MethodCallback, funcData); auto func = funcTemplate->GetFunction(context).ToLocalChecked(); - std::string origin = Constants::APP_ROOT_FOLDER_PATH + GetOrCreateInternal(treeNode)->m_name; + std::string origin = AppFiles::AppRootPrefix() + GetOrCreateInternal(treeNode)->m_name; Local wrapperFunc = Wrap(isolate, func, methodName, origin, false /* isCtorFunc */); Local ctorFunc = ctorFuncTemplate->GetFunction(context).ToLocalChecked(); @@ -1069,7 +1070,7 @@ void MetadataNode::SetStaticMembers(Isolate* isolate, Local& ctorFunct MethodCallbackData* callbackData = nullptr; auto cache = GetMetadataNodeCache(isolate); - auto origin = Constants::APP_ROOT_FOLDER_PATH + GetOrCreateInternal(treeNode)->m_name; + auto origin = AppFiles::AppRootPrefix() + GetOrCreateInternal(treeNode)->m_name; //get candidates from static methods metadata auto staticMethodCout = *reinterpret_cast(curPtr); @@ -1286,7 +1287,7 @@ Local MetadataNode::GetConstructorFunctionTemplate(Isolate* is auto context = isolate->GetCurrentContext(); auto ctorFunc = ctorFuncTemplate->GetFunction(context).ToLocalChecked(); - auto origin = Constants::APP_ROOT_FOLDER_PATH + node->m_name; + auto origin = AppFiles::AppRootPrefix() + node->m_name; auto wrappedCtorFunc = Wrap(isolate, ctorFunc, node->m_treeNode->name, origin, true /* isCtorFunc */); @@ -2123,7 +2124,7 @@ bool MetadataNode::GetExtendLocation(v8::Isolate* isolate, string& extendLocatio normalized.resize(queryOrFragment); } - const string& appRoot = Constants::APP_ROOT_FOLDER_PATH; + const string& appRoot = AppFiles::AppRootPrefix(); if (!appRoot.empty()) { stripPrefix(normalized, appRoot); } diff --git a/test-app/runtime/src/main/cpp/ModuleInternal.cpp b/test-app/runtime/src/main/cpp/ModuleInternal.cpp index 31fae2222..2701fbd01 100644 --- a/test-app/runtime/src/main/cpp/ModuleInternal.cpp +++ b/test-app/runtime/src/main/cpp/ModuleInternal.cpp @@ -5,6 +5,7 @@ * Author: gatanasov */ #include "ModuleInternal.h" +#include "AppFiles.h" #include "ModuleInternalCallbacks.h" #include "BuiltinLoader.h" #include "File.h" @@ -169,8 +170,6 @@ void ModuleInternal::Init(Isolate* isolate, const string& baseDir) { RESOLVE_PATH_METHOD_ID = env.GetStaticMethodID(MODULE_CLASS, "resolvePath", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"); NS_CHECK(RESOLVE_PATH_METHOD_ID != nullptr); - GET_APPLICATION_FILES_PATH_METHOD_ID = env.GetStaticMethodID(MODULE_CLASS, "getApplicationFilesPath", "()Ljava/lang/String;"); - NS_CHECK(GET_APPLICATION_FILES_PATH_METHOD_ID != nullptr); } m_isolate = isolate; @@ -209,7 +208,7 @@ void ModuleInternal::Init(Isolate* isolate, const string& baseDir) { globalRequire = GetRequireFunction(isolate, baseDir, RequireEvaluationOptions( ModuleEvaluationPolicy::kSyncStrict)); } else { - globalRequire = GetRequireFunction(isolate, Constants::APP_ROOT_FOLDER_PATH, + globalRequire = GetRequireFunction(isolate, AppFiles::AppRootPrefix(), RequireEvaluationOptions( ModuleEvaluationPolicy::kSyncStrict)); } @@ -684,7 +683,7 @@ Local ModuleInternal::LoadImpl(Isolate* isolate, const string& moduleNam if (!moduleName.empty() && moduleName[0] == '~') { // Convert ~/path to ApplicationPath/path std::string tail = moduleName.size() >= 2 && moduleName[1] == '/' ? moduleName.substr(2) : moduleName.substr(1); - resolvedModuleName = Constants::APP_ROOT_FOLDER_PATH + "/" + tail; + resolvedModuleName = AppFiles::AppRootPrefix() + tail; // For .mjs files with tilde paths, use resolved path directly if (Util::EndsWith(resolvedModuleName, ".mjs")) { @@ -1706,7 +1705,6 @@ ModuleInternal::ModulePathKind ModuleInternal::GetModulePathKind(const std::stri jclass ModuleInternal::MODULE_CLASS = nullptr; jmethodID ModuleInternal::RESOLVE_PATH_METHOD_ID = nullptr; -jmethodID ModuleInternal::GET_APPLICATION_FILES_PATH_METHOD_ID = nullptr; const char* ModuleInternal::MODULE_PROLOGUE = "(function(module, exports, require, __filename, __dirname){ "; const char* ModuleInternal::MODULE_EPILOGUE = "\n})"; diff --git a/test-app/runtime/src/main/cpp/ModuleInternal.h b/test-app/runtime/src/main/cpp/ModuleInternal.h index e3306fa38..a815ce0a3 100644 --- a/test-app/runtime/src/main/cpp/ModuleInternal.h +++ b/test-app/runtime/src/main/cpp/ModuleInternal.h @@ -247,7 +247,6 @@ class ModuleInternal { public: static jclass MODULE_CLASS; static jmethodID RESOLVE_PATH_METHOD_ID; - static jmethodID GET_APPLICATION_FILES_PATH_METHOD_ID; static const char* MODULE_PROLOGUE; static const char* MODULE_EPILOGUE; diff --git a/test-app/runtime/src/main/cpp/ModuleInternalCallbacks.cpp b/test-app/runtime/src/main/cpp/ModuleInternalCallbacks.cpp index f83007769..cf9afdff7 100644 --- a/test-app/runtime/src/main/cpp/ModuleInternalCallbacks.cpp +++ b/test-app/runtime/src/main/cpp/ModuleInternalCallbacks.cpp @@ -16,6 +16,7 @@ #include #include +#include "AppFiles.h" #include "ArgConverter.h" #include "EventLoop.h" #include "HttpLoader.h" @@ -77,25 +78,6 @@ static std::string WithExtension(const std::string& path, const std::string& ext return path + ext; } -// Application filesystem root for on-disk .mjs/.js resolution. -// Mirrors Module.java's getApplicationFilesPath + "/app". Cached after first -// JNI call — the value is process-stable, and re-entering JNI on every -// resolver hit would add avoidable overhead to hot module-graph walks. -static std::string GetApplicationPath() { - static std::string cached; - static std::once_flag flag; - std::call_once(flag, []() { - JEnv env; - jstring applicationFilesPath = (jstring)env.CallStaticObjectMethod( - ModuleInternal::MODULE_CLASS, - ModuleInternal::GET_APPLICATION_FILES_PATH_METHOD_ID); - if (applicationFilesPath != nullptr) { - cached = ArgConverter::jstringToString(applicationFilesPath) + "/app"; - } - }); - return cached; -} - // Collapse "." and ".." segments, preserving a leading "/". static std::string NormalizeDotSegments(const std::string& path) { std::vector stack; @@ -1191,7 +1173,7 @@ static ModuleResolution ResolveSpecifierToPath(const std::string& rawSpec, if (referrer.empty() && specIsRelative) { TNS_DEBUG(Esm, "[resolver] No referrer for relative '%s' - assuming app root", spec.c_str()); - referrer = GetApplicationPath() + "/index.mjs"; + referrer = AppFiles::AppRootPrefix() + "index.mjs"; } size_t slash = referrer.find_last_of("/\\"); const std::string baseDir = @@ -1234,7 +1216,7 @@ static ModuleResolution ResolveSpecifierToPath(const std::string& rawSpec, // Build the filesystem candidates for this specifier shape. The specifier may // omit its extension or name a directory, so each candidate is probed with // Node-style extension and index fallbacks below. - const std::string appPath = GetApplicationPath(); + const std::string& appPath = AppFiles::AppRoot(); std::vector candidateBases; if (specIsRelative) { diff --git a/test-app/runtime/src/main/cpp/Runtime.cpp b/test-app/runtime/src/main/cpp/Runtime.cpp index c7d36ae87..15f1453c4 100644 --- a/test-app/runtime/src/main/cpp/Runtime.cpp +++ b/test-app/runtime/src/main/cpp/Runtime.cpp @@ -1,4 +1,5 @@ #include "Runtime.h" +#include "AppFiles.h" #include @@ -254,7 +255,7 @@ void Runtime::Init(JNIEnv* env, jstring filesPath, jstring nativeLibDir, CrashBreadcrumbs::OpenStore(filesRoot); CrashBreadcrumbs::RegisterRuntime(m_id); - Constants::APP_ROOT_FOLDER_PATH = filesRoot + "/app/"; + AppFiles::Init(filesRoot); Constants::V8_STARTUP_FLAGS = options.v8Flags; Constants::V8_CACHE_COMPILED_CODE = options.codeCache; m_discardUncaughtJsExceptions = options.discardUncaughtJsExceptions; From 1e644cd9259e38cd00b84c7bcaf7ba270691b096 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Wed, 11 Nov 2026 14:19:36 -0300 Subject: [PATCH 2/8] refactor: native AppFiles, disk only Give AppFiles the runtime's app file reads: IsFile, ReadText and Open (a Reader with size() and read() into caller-owned buffers). A path under /app, /internal or /metadata is normalized - "//", "." and ".." collapsed, /data/data/ and /data/user/0/ spellings mapped onto the canonical files root - and served from an ordered list of sources; anything else reads the real file system unchanged. The only source for now is the files directory itself. Route through it: module stat/read for CommonJS, ES modules and JSON (ModuleInternal, ModuleInternalCallbacks), Runtime::RunScript and ReadFileText (still under the debug file-write lock), the inspector's package.json and network-resource reads, and the metadata streams, which are read into the same caller-allocated buffers. The direct-boot opendir guard and .so dlopen paths are unchanged. --- test-app/runtime/src/main/cpp/AppFiles.cpp | 271 +++++++++++++++++- test-app/runtime/src/main/cpp/AppFiles.h | 45 ++- .../src/main/cpp/JsV8InspectorClient.cpp | 16 +- .../runtime/src/main/cpp/MetadataNode.cpp | 37 +-- .../runtime/src/main/cpp/ModuleInternal.cpp | 3 +- .../src/main/cpp/ModuleInternalCallbacks.cpp | 7 +- test-app/runtime/src/main/cpp/Runtime.cpp | 16 +- 7 files changed, 334 insertions(+), 61 deletions(-) diff --git a/test-app/runtime/src/main/cpp/AppFiles.cpp b/test-app/runtime/src/main/cpp/AppFiles.cpp index 22498f83f..f7b715290 100644 --- a/test-app/runtime/src/main/cpp/AppFiles.cpp +++ b/test-app/runtime/src/main/cpp/AppFiles.cpp @@ -1,35 +1,290 @@ #include "AppFiles.h" +#include +#include +#include #include +#include +#include namespace tns { namespace AppFiles { namespace { -struct Roots { +// One place app files can come from. `relative` is relative to the files +// root, e.g. "app/main.js" or "metadata/treeNodeStream.dat". +class Source { +public: + virtual ~Source() = default; + virtual bool IsFile(const std::string& relative) const = 0; + // Null with errno ENOENT or ENOTDIR when this source has no such file. + virtual std::unique_ptr Open(const std::string& relative) const = 0; +}; + +class FileReader final : public Reader { +public: + FileReader(int fd, size_t size) : m_fd(fd), m_size(size) {} + ~FileReader() override { close(m_fd); } + + size_t size() const override { return m_size; } + + size_t read(void* dst, size_t n) override { + auto* out = static_cast(dst); + size_t total = 0; + while (total < n) { + ssize_t r = ::read(m_fd, out + total, n - total); + if (r < 0 && errno == EINTR) { + continue; + } + if (r <= 0) { + break; + } + total += static_cast(r); + } + return total; + } + +private: + int m_fd; + size_t m_size; +}; + +bool IsRegularFile(const std::string& path) { + struct stat st; + return stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode); +} + +std::unique_ptr OpenRegularFile(const std::string& path) { + int fd = open(path.c_str(), O_RDONLY | O_CLOEXEC); + if (fd < 0) { + return nullptr; + } + struct stat st; + int error = 0; + if (fstat(fd, &st) != 0) { + error = errno; + } else if (!S_ISREG(st.st_mode)) { + error = S_ISDIR(st.st_mode) ? EISDIR : EINVAL; + } + if (error != 0) { + close(fd); + errno = error; + return nullptr; + } + return std::unique_ptr(new FileReader(fd, static_cast(st.st_size))); +} + +class DirectorySource final : public Source { +public: + explicit DirectorySource(std::string root) : m_root(std::move(root)) {} + + bool IsFile(const std::string& relative) const override { + return IsRegularFile(m_root + "/" + relative); + } + + std::unique_ptr Open(const std::string& relative) const override { + return OpenRegularFile(m_root + "/" + relative); + } + +private: + std::string m_root; +}; + +struct State { + std::string files; std::string app; std::string appPrefix; + std::string internal; + std::string metadata; + // Spellings of `files` other than itself. + std::vector aliases; + // Searched in order; the first source that has a file serves it. + std::vector> sources; }; -Roots s_roots; +State s_state; std::once_flag s_initOnce; +// Collapses "//", "." and ".." in an absolute path; ".." never climbs above "/". +std::string CollapseDotSegments(const std::string& path) { + std::vector segments; + size_t i = 0; + while (i <= path.size()) { + size_t j = path.find('/', i); + if (j == std::string::npos) { + j = path.size(); + } + std::string segment = path.substr(i, j - i); + if (segment == "..") { + if (!segments.empty()) { + segments.pop_back(); + } + } else if (!segment.empty() && segment != ".") { + segments.push_back(std::move(segment)); + } + i = j + 1; + } + std::string result; + for (const auto& segment : segments) { + result += "/"; + result += segment; + } + return result.empty() ? "/" : result; +} + +bool HasDirPrefix(const std::string& path, const std::string& dir) { + return path.size() > dir.size() && path.compare(0, dir.size(), dir) == 0 && + path[dir.size()] == '/'; +} + +// /data/data/ and /data/user/0/ name the same directory. +std::string SwappedDataSpelling(const std::string& path) { + static const std::string kDataData = "/data/data/"; + static const std::string kDataUser0 = "/data/user/0/"; + if (path.compare(0, kDataData.size(), kDataData) == 0) { + return kDataUser0 + path.substr(kDataData.size()); + } + if (path.compare(0, kDataUser0.size(), kDataUser0) == 0) { + return kDataData + path.substr(kDataUser0.size()); + } + return std::string(); +} + +void AddAlias(const std::string& alias) { + if (alias.empty() || alias[0] != '/') { + return; + } + std::string collapsed = CollapseDotSegments(alias); + if (collapsed == s_state.files) { + return; + } + for (const auto& existing : s_state.aliases) { + if (existing == collapsed) { + return; + } + } + s_state.aliases.push_back(std::move(collapsed)); +} + +// The path relative to the files root when `path` names something under one +// of the layered roots (app, internal, metadata). +bool LayeredRelativePath(const std::string& path, std::string& relative) { + if (s_state.files.empty()) { + return false; + } + std::string logical = Normalize(path); + if (!HasDirPrefix(logical, s_state.files)) { + return false; + } + std::string rest = logical.substr(s_state.files.size() + 1); + for (const char* root : {"app/", "internal/", "metadata/"}) { + if (rest.compare(0, strlen(root), root) == 0) { + relative = std::move(rest); + return true; + } + } + return false; +} + } // namespace -void Init(const std::string& filesRoot) { - std::call_once(s_initOnce, [&filesRoot]() { - s_roots.app = filesRoot + "/app"; - s_roots.appPrefix = s_roots.app + "/"; +void Init(const std::string& filesRoot, const std::vector& aliases) { + std::call_once(s_initOnce, [&]() { + s_state.files = filesRoot.empty() || filesRoot[0] != '/' + ? filesRoot + : CollapseDotSegments(filesRoot); + s_state.app = s_state.files + "/app"; + s_state.appPrefix = s_state.app + "/"; + s_state.internal = s_state.files + "/internal"; + s_state.metadata = s_state.files + "/metadata"; + + AddAlias(SwappedDataSpelling(s_state.files)); + for (const auto& alias : aliases) { + AddAlias(alias); + AddAlias(SwappedDataSpelling(alias)); + } + + s_state.sources.emplace_back(new DirectorySource(s_state.files)); }); } +const std::string& FilesRoot() { + return s_state.files; +} + const std::string& AppRoot() { - return s_roots.app; + return s_state.app; +} + +const std::string& InternalRoot() { + return s_state.internal; +} + +const std::string& MetadataRoot() { + return s_state.metadata; } const std::string& AppRootPrefix() { - return s_roots.appPrefix; + return s_state.appPrefix; +} + +std::string Normalize(const std::string& path) { + if (s_state.files.empty() || path.empty() || path[0] != '/') { + return path; + } + std::string collapsed = CollapseDotSegments(path); + if (collapsed == s_state.files || HasDirPrefix(collapsed, s_state.files)) { + return collapsed; + } + for (const auto& alias : s_state.aliases) { + if (collapsed == alias) { + return s_state.files; + } + if (HasDirPrefix(collapsed, alias)) { + return s_state.files + collapsed.substr(alias.size()); + } + } + return path; +} + +bool IsFile(const std::string& path) { + std::string relative; + if (!LayeredRelativePath(path, relative)) { + return IsRegularFile(path); + } + for (const auto& source : s_state.sources) { + if (source->IsFile(relative)) { + return true; + } + } + return false; +} + +std::unique_ptr Open(const std::string& path) { + std::string relative; + if (!LayeredRelativePath(path, relative)) { + return OpenRegularFile(path); + } + errno = ENOENT; + for (const auto& source : s_state.sources) { + auto reader = source->Open(relative); + if (reader != nullptr || (errno != ENOENT && errno != ENOTDIR)) { + return reader; + } + } + return nullptr; +} + +bool ReadText(const std::string& path, std::string& out) { + auto reader = Open(path); + if (reader == nullptr) { + out.clear(); + return false; + } + out.resize(reader->size()); + out.resize(reader->read(&out[0], out.size())); + return true; } } // namespace AppFiles diff --git a/test-app/runtime/src/main/cpp/AppFiles.h b/test-app/runtime/src/main/cpp/AppFiles.h index 77bdef2ec..ff2a3929b 100644 --- a/test-app/runtime/src/main/cpp/AppFiles.h +++ b/test-app/runtime/src/main/cpp/AppFiles.h @@ -1,23 +1,60 @@ #ifndef APPFILES_H_ #define APPFILES_H_ +#include +#include #include +#include namespace tns { namespace AppFiles { /* - * Sets the app's files directory. Only the first call takes effect, so every - * runtime may call it; the roots below are empty until then. + * Sets the app's files directory: the canonical path, which is the logical + * root V8 sees, plus any other spellings of it. Only the first call takes + * effect, so every runtime may call it; the roots below are empty until then. */ -void Init(const std::string& filesRoot); +void Init(const std::string& filesRoot, const std::vector& aliases = {}); -// "/app", no trailing slash. +// The logical "", "/app", "/internal" and "/metadata", +// with no trailing slash. +const std::string& FilesRoot(); const std::string& AppRoot(); +const std::string& InternalRoot(); +const std::string& MetadataRoot(); // "/app/", for prefix checks and joining relative names. const std::string& AppRootPrefix(); +/* + * The logical spelling of `path`: "." / ".." / "//" are collapsed and any + * alias of the files directory becomes FilesRoot(). A path that does not end + * up inside the files directory is returned unchanged. + */ +std::string Normalize(const std::string& path); + +/* + * Reads of app files. A path under the app, internal or metadata root is + * resolved through the app file sources; anything else reads the real file + * system as-is. Safe to call from any thread. + */ +bool IsFile(const std::string& path); + +// False when the file cannot be opened; a short read truncates `out`. +bool ReadText(const std::string& path, std::string& out); + +class Reader { +public: + virtual ~Reader() = default; + virtual size_t size() const = 0; + // Fills `dst` with up to `n` bytes; fewer only at end of file or on error. + virtual size_t read(void* dst, size_t n) = 0; +}; + +// Null when the file cannot be opened, with errno describing why. +// A Reader belongs to one thread at a time. +std::unique_ptr Open(const std::string& path); + } // namespace AppFiles } // namespace tns diff --git a/test-app/runtime/src/main/cpp/JsV8InspectorClient.cpp b/test-app/runtime/src/main/cpp/JsV8InspectorClient.cpp index eb1713094..6a64f079c 100644 --- a/test-app/runtime/src/main/cpp/JsV8InspectorClient.cpp +++ b/test-app/runtime/src/main/cpp/JsV8InspectorClient.cpp @@ -1,5 +1,4 @@ #include "JsV8InspectorClient.h" -#include "AppFiles.h" #include #include #include @@ -17,9 +16,9 @@ #include "NativeScriptAssert.h" #include "NativeScriptPlatform.h" +#include "AppFiles.h" #include "ArgConverter.h" #include "Constants.h" -#include "File.h" #include "Util.h" #include "Utils.h" #include "WorkerInspectorClient.h" @@ -196,13 +195,10 @@ bool GetInt32(const json& value, long long& out) { // key at the top level. bool ShouldRewriteSourceMapURLs() { static bool disabled = []() { - int length = 0; - auto* data = static_cast(File::ReadBinary(AppFiles::AppRootPrefix() + "package.json", length)); - if (data == nullptr) { + std::string content; + if (!AppFiles::ReadText(AppFiles::AppRootPrefix() + "package.json", content)) { return false; } - std::string content(reinterpret_cast(data), length); - delete[] data; auto parsed = json::parse(content, nullptr, false); if (parsed.is_discarded() || !parsed.is_object()) { @@ -859,11 +855,7 @@ std::string JsV8InspectorClient::HandleLoadNetworkResource(long long msgId, cons } for (const auto& candidate : candidates) { - int length = 0; - auto* data = static_cast(File::ReadBinary(candidate, length)); - if (data != nullptr) { - content.assign(reinterpret_cast(data), length); - delete[] data; + if (AppFiles::ReadText(candidate, content)) { loaded = true; break; } diff --git a/test-app/runtime/src/main/cpp/MetadataNode.cpp b/test-app/runtime/src/main/cpp/MetadataNode.cpp index 70995b419..404c1dbe9 100644 --- a/test-app/runtime/src/main/cpp/MetadataNode.cpp +++ b/test-app/runtime/src/main/cpp/MetadataNode.cpp @@ -2253,8 +2253,8 @@ void MetadataNode::BuildMetadata(const string& filesPath) { string namesFile = baseDir + "/treeStringsStream.dat"; string valuesFile = baseDir + "/treeValueStream.dat"; - FILE* f = fopen(nodesFile.c_str(), "rb"); - if (f == nullptr) { + auto nodesReader = AppFiles::Open(nodesFile); + if (nodesReader == nullptr) { stringstream ss; ss << "metadata file (treeNodeStream.dat) couldn't be opened! (Error: "; ss << errno; @@ -2262,11 +2262,9 @@ void MetadataNode::BuildMetadata(const string& filesPath) { throw NativeScriptException(ss.str()); } - fseek(f, 0, SEEK_END); - int lenNodes = ftell(f); + int lenNodes = static_cast(nodesReader->size()); if (lenNodes < 0 || (static_cast(lenNodes) % sizeof(MetadataTreeNodeRawData)) != 0) { - fclose(f); stringstream ss; ss << "Metadata file " << nodesFile << " is " << lenNodes << " bytes, which is not a whole number of " @@ -2275,41 +2273,36 @@ void MetadataNode::BuildMetadata(const string& filesPath) { throw NativeScriptException(ss.str()); } char* nodes = new char[lenNodes]; - rewind(f); - fread(nodes, 1, lenNodes, f); - fclose(f); + nodesReader->read(nodes, lenNodes); + nodesReader.reset(); const int _512KB = 524288; - f = fopen(namesFile.c_str(), "rb"); - if (f == nullptr) { + auto namesReader = AppFiles::Open(namesFile); + if (namesReader == nullptr) { stringstream ss; ss << "metadata file (treeStringsStream.dat) couldn't be opened! (Error: "; ss << errno; ss << ") "; throw NativeScriptException(ss.str()); } - fseek(f, 0, SEEK_END); - int lenNames = ftell(f); + int lenNames = static_cast(namesReader->size()); char* names = new char[lenNames + _512KB]; - rewind(f); - fread(names, 1, lenNames, f); - fclose(f); + namesReader->read(names, lenNames); + namesReader.reset(); - f = fopen(valuesFile.c_str(), "rb"); - if (f == nullptr) { + auto valuesReader = AppFiles::Open(valuesFile); + if (valuesReader == nullptr) { stringstream ss; ss << "metadata file (treeValueStream.dat) couldn't be opened! (Error: "; ss << errno; ss << ") "; throw NativeScriptException(ss.str()); } - fseek(f, 0, SEEK_END); - int lenValues = ftell(f); + int lenValues = static_cast(valuesReader->size()); char* values = new char[lenValues + _512KB]; - rewind(f); - fread(values, 1, lenValues, f); - fclose(f); + valuesReader->read(values, lenValues); + valuesReader.reset(); timeval time2; gettimeofday(&time2, nullptr); diff --git a/test-app/runtime/src/main/cpp/ModuleInternal.cpp b/test-app/runtime/src/main/cpp/ModuleInternal.cpp index 2701fbd01..429de6750 100644 --- a/test-app/runtime/src/main/cpp/ModuleInternal.cpp +++ b/test-app/runtime/src/main/cpp/ModuleInternal.cpp @@ -1049,8 +1049,7 @@ MaybeLocal ModuleInternal::CompileFileEsModule(Isolate* isolate, const s // routes (app main, worker main) reach here straight from the caller's // specifier — so the existence check has to live here, or a missing entry // reads a null FILE* instead of failing with a name. - struct stat st; - if (stat(path.c_str(), &st) != 0 || !S_ISREG(st.st_mode)) { + if (!AppFiles::IsFile(path)) { throw NativeScriptException("Cannot find module " + path); } diff --git a/test-app/runtime/src/main/cpp/ModuleInternalCallbacks.cpp b/test-app/runtime/src/main/cpp/ModuleInternalCallbacks.cpp index cf9afdff7..ff04ae301 100644 --- a/test-app/runtime/src/main/cpp/ModuleInternalCallbacks.cpp +++ b/test-app/runtime/src/main/cpp/ModuleInternalCallbacks.cpp @@ -60,13 +60,8 @@ static inline v8::Local ToV8String(v8::Isolate* isolate, return ArgConverter::ConvertToV8String(isolate, str); } -// Filesystem: `path` names an existing regular file. static bool IsFile(const std::string& path) { - struct stat st; - if (stat(path.c_str(), &st) != 0) { - return false; - } - return (st.st_mode & S_IFMT) == S_IFREG; + return AppFiles::IsFile(path); } // Append `ext` if `path` doesn't already carry it. diff --git a/test-app/runtime/src/main/cpp/Runtime.cpp b/test-app/runtime/src/main/cpp/Runtime.cpp index 15f1453c4..fa54f28e4 100644 --- a/test-app/runtime/src/main/cpp/Runtime.cpp +++ b/test-app/runtime/src/main/cpp/Runtime.cpp @@ -1,5 +1,4 @@ #include "Runtime.h" -#include "AppFiles.h" #include @@ -11,6 +10,7 @@ #include #include +#include "AppFiles.h" #include "ArgConverter.h" #include "BuiltinLoader.h" #include "CallbackHandlers.h" @@ -19,7 +19,6 @@ #include "ErrorEvents.h" #include "EventLoop.h" #include "Events.h" -#include "File.h" #include "FrameCallbacks.h" #include "HttpLoader.h" #include "IsolateTracked.h" @@ -330,17 +329,20 @@ Runtime::~Runtime() { } std::string Runtime::ReadFileText(const std::string& filePath) { -#ifdef APPLICATION_IN_DEBUG - std::lock_guard lock(m_fileWriteMutex); -#endif - return File::ReadText(filePath); + bool ok; + return ReadFileText(filePath, ok); } std::string Runtime::ReadFileText(const std::string& filePath, bool& ok) { #ifdef APPLICATION_IN_DEBUG std::lock_guard lock(m_fileWriteMutex); #endif - return File::ReadText(filePath, ok); + std::string content; + ok = AppFiles::ReadText(filePath, content); + if (!ok) { + DEBUG_WRITE_FORCE("Runtime::ReadFileText: cannot open %s", filePath.c_str()); + } + return content; } void Runtime::Lock() { From 660e4a368040e6b651b0e1b758a68906f7cefe18 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Wed, 11 Nov 2026 14:24:32 -0300 Subject: [PATCH 3/8] refactor: Java onto AppFiles natives Add com.tns.AppFiles: static natives over the native AppFiles (init, filesRoot, appRoot, logicalPath, isFile, readText) that need only the loaded library, not an initialized Runtime. RuntimeHelper initializes it right after System.loadLibrary, before AppConfig is built; Module.init initializes it too for embedders that start the runtime another way. Route the Java-side reads through it: Module's file/directory probing, package.json reading and folder-module cache keys, AppConfig's package.json, Runtime.runScript's existence check and RuntimeHelper's tns-java-classes.js check. package.json is now decoded as UTF-8 rather than with the platform default charset. Behaviour change: Module.resolvePath canonicalizes only the files root. Paths inside it are normalized lexically ("//", "." and ".." collapsed, /data/data and /data/user/0 spellings unified), so symlinks under the app directory are no longer resolved to their targets. Paths outside the files directory are still canonicalized. --- .../src/main/java/com/tns/RuntimeHelper.java | 3 +- test-app/runtime/CMakeLists.txt | 1 + test-app/runtime/src/main/cpp/AppFiles.cpp | 21 ++-- test-app/runtime/src/main/cpp/AppFiles.h | 8 +- .../runtime/src/main/cpp/com_tns_AppFiles.cpp | 78 +++++++++++++ .../src/main/java/com/tns/AppConfig.java | 6 +- .../src/main/java/com/tns/AppFiles.java | 55 +++++++++ .../runtime/src/main/java/com/tns/Module.java | 107 ++++++++---------- .../src/main/java/com/tns/Runtime.java | 2 +- 9 files changed, 204 insertions(+), 77 deletions(-) create mode 100644 test-app/runtime/src/main/cpp/com_tns_AppFiles.cpp create mode 100644 test-app/runtime/src/main/java/com/tns/AppFiles.java diff --git a/test-app/app/src/main/java/com/tns/RuntimeHelper.java b/test-app/app/src/main/java/com/tns/RuntimeHelper.java index fa1542966..92d693be5 100644 --- a/test-app/app/src/main/java/com/tns/RuntimeHelper.java +++ b/test-app/app/src/main/java/com/tns/RuntimeHelper.java @@ -71,6 +71,7 @@ public static Runtime initRuntime(Context context) { } finally { loadLibraryFrame.close(); } + AppFiles.init(context.getFilesDir()); Logger logger = new LogcatLogger(context); @@ -205,7 +206,7 @@ public static Runtime initRuntime(Context context) { // runtime.runScript(new File(appDir, "internal/ts_helpers.js")); File javaClassesModule = new File(appDir, "app/tns-java-classes.js"); - if (javaClassesModule.exists()) { + if (AppFiles.isFile(javaClassesModule.getAbsolutePath())) { runtime.runModule(javaClassesModule); } diff --git a/test-app/runtime/CMakeLists.txt b/test-app/runtime/CMakeLists.txt index 5f250d7fb..d72061889 100644 --- a/test-app/runtime/CMakeLists.txt +++ b/test-app/runtime/CMakeLists.txt @@ -238,6 +238,7 @@ add_library( src/main/cpp/WorkerEvents.cpp src/main/cpp/WorkerWrapper.cpp src/main/cpp/Timers.cpp + src/main/cpp/com_tns_AppFiles.cpp src/main/cpp/com_tns_AssetExtractor.cpp src/main/cpp/com_tns_Runtime.cpp src/main/cpp/console/Console.cpp diff --git a/test-app/runtime/src/main/cpp/AppFiles.cpp b/test-app/runtime/src/main/cpp/AppFiles.cpp index f7b715290..c5be23bbf 100644 --- a/test-app/runtime/src/main/cpp/AppFiles.cpp +++ b/test-app/runtime/src/main/cpp/AppFiles.cpp @@ -173,8 +173,8 @@ bool LayeredRelativePath(const std::string& path, std::string& relative) { if (s_state.files.empty()) { return false; } - std::string logical = Normalize(path); - if (!HasDirPrefix(logical, s_state.files)) { + std::string logical; + if (!ToLogical(path, logical) || !HasDirPrefix(logical, s_state.files)) { return false; } std::string rest = logical.substr(s_state.files.size() + 1); @@ -229,23 +229,22 @@ const std::string& AppRootPrefix() { return s_state.appPrefix; } -std::string Normalize(const std::string& path) { +bool ToLogical(const std::string& path, std::string& logical) { if (s_state.files.empty() || path.empty() || path[0] != '/') { - return path; + return false; } std::string collapsed = CollapseDotSegments(path); if (collapsed == s_state.files || HasDirPrefix(collapsed, s_state.files)) { - return collapsed; + logical = std::move(collapsed); + return true; } for (const auto& alias : s_state.aliases) { - if (collapsed == alias) { - return s_state.files; - } - if (HasDirPrefix(collapsed, alias)) { - return s_state.files + collapsed.substr(alias.size()); + if (collapsed == alias || HasDirPrefix(collapsed, alias)) { + logical = s_state.files + collapsed.substr(alias.size()); + return true; } } - return path; + return false; } bool IsFile(const std::string& path) { diff --git a/test-app/runtime/src/main/cpp/AppFiles.h b/test-app/runtime/src/main/cpp/AppFiles.h index ff2a3929b..62c42b98d 100644 --- a/test-app/runtime/src/main/cpp/AppFiles.h +++ b/test-app/runtime/src/main/cpp/AppFiles.h @@ -27,11 +27,11 @@ const std::string& MetadataRoot(); const std::string& AppRootPrefix(); /* - * The logical spelling of `path`: "." / ".." / "//" are collapsed and any - * alias of the files directory becomes FilesRoot(). A path that does not end - * up inside the files directory is returned unchanged. + * True when `path` names something inside the files directory under any of + * its spellings. `logical` is then that path with "//", "." and ".." + * collapsed and the directory spelled as FilesRoot(). */ -std::string Normalize(const std::string& path); +bool ToLogical(const std::string& path, std::string& logical); /* * Reads of app files. A path under the app, internal or metadata root is diff --git a/test-app/runtime/src/main/cpp/com_tns_AppFiles.cpp b/test-app/runtime/src/main/cpp/com_tns_AppFiles.cpp new file mode 100644 index 000000000..5a9350739 --- /dev/null +++ b/test-app/runtime/src/main/cpp/com_tns_AppFiles.cpp @@ -0,0 +1,78 @@ +#include + +#include +#include + +#include "AppFiles.h" + +using namespace tns; + +namespace { + +std::string ToStdString(JNIEnv* env, jstring value) { + if (value == nullptr) { + return std::string(); + } + const char* chars = env->GetStringUTFChars(value, nullptr); + if (chars == nullptr) { + return std::string(); + } + std::string result(chars); + env->ReleaseStringUTFChars(value, chars); + return result; +} + +// Paths are Modified UTF-8 on the way in and out, matching how the rest of the +// runtime converts jstrings; file contents travel as bytes instead. +jstring ToJString(JNIEnv* env, const std::string& value) { + return env->NewStringUTF(value.c_str()); +} + +} // namespace + +extern "C" JNIEXPORT void JNICALL Java_com_tns_AppFiles_nativeInit(JNIEnv* env, jclass, + jstring filesRoot, + jstring alias) { + std::vector aliases; + if (alias != nullptr) { + aliases.push_back(ToStdString(env, alias)); + } + AppFiles::Init(ToStdString(env, filesRoot), aliases); +} + +extern "C" JNIEXPORT jstring JNICALL Java_com_tns_AppFiles_filesRoot(JNIEnv* env, jclass) { + return ToJString(env, AppFiles::FilesRoot()); +} + +extern "C" JNIEXPORT jstring JNICALL Java_com_tns_AppFiles_appRoot(JNIEnv* env, jclass) { + return ToJString(env, AppFiles::AppRoot()); +} + +extern "C" JNIEXPORT jstring JNICALL Java_com_tns_AppFiles_logicalPath(JNIEnv* env, jclass, + jstring path) { + std::string logical; + if (!AppFiles::ToLogical(ToStdString(env, path), logical)) { + return nullptr; + } + return ToJString(env, logical); +} + +extern "C" JNIEXPORT jboolean JNICALL Java_com_tns_AppFiles_isFile(JNIEnv* env, jclass, + jstring path) { + return AppFiles::IsFile(ToStdString(env, path)) ? JNI_TRUE : JNI_FALSE; +} + +extern "C" JNIEXPORT jbyteArray JNICALL Java_com_tns_AppFiles_readBytes(JNIEnv* env, jclass, + jstring path) { + std::string content; + if (!AppFiles::ReadText(ToStdString(env, path), content)) { + return nullptr; + } + jbyteArray result = env->NewByteArray(static_cast(content.size())); + if (result == nullptr) { + return nullptr; + } + env->SetByteArrayRegion(result, 0, static_cast(content.size()), + reinterpret_cast(content.data())); + return result; +} diff --git a/test-app/runtime/src/main/java/com/tns/AppConfig.java b/test-app/runtime/src/main/java/com/tns/AppConfig.java index 7d7dcd086..54502c42e 100644 --- a/test-app/runtime/src/main/java/com/tns/AppConfig.java +++ b/test-app/runtime/src/main/java/com/tns/AppConfig.java @@ -52,14 +52,14 @@ public Object getDefaultValue() { public AppConfig(File appDir) { values = makeDefaultOptions(); - File packageInfo = new File(appDir, "/app/package.json"); - if (!packageInfo.exists()) { + String packageText = AppFiles.readText(new File(appDir, "app/package.json").getAbsolutePath()); + if (packageText == null) { return; } JSONObject rootObject; try { - rootObject = FileSystem.readJSONFile(packageInfo); + rootObject = new JSONObject(packageText); if (rootObject != null) { // Parse security configuration if (rootObject.has(SecurityKey)) { diff --git a/test-app/runtime/src/main/java/com/tns/AppFiles.java b/test-app/runtime/src/main/java/com/tns/AppFiles.java new file mode 100644 index 000000000..8693352d0 --- /dev/null +++ b/test-app/runtime/src/main/java/com/tns/AppFiles.java @@ -0,0 +1,55 @@ +package com.tns; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * The runtime's view of the app's files: the same sources and path rules the + * native module loader uses. Usable once libNativeScript is loaded; it needs no + * initialized Runtime. + */ +public final class AppFiles { + private AppFiles() { + } + + /** + * Sets the files directory. Only the first call takes effect, so every + * caller that may run first can make it. + */ + public static void init(File filesDir) { + String absolute = filesDir.getAbsolutePath(); + String canonical; + try { + canonical = filesDir.getCanonicalPath(); + } catch (IOException e) { + canonical = absolute; + } + nativeInit(canonical, absolute); + } + + /** UTF-8 contents, or null when the file cannot be read. */ + public static String readText(String path) { + byte[] bytes = readBytes(path); + return bytes != null ? new String(bytes, StandardCharsets.UTF_8) : null; + } + + /** The canonical files directory, which every logical path starts with. */ + public static native String filesRoot(); + + /** "<files>/app", no trailing slash. */ + public static native String appRoot(); + + /** + * The logical spelling of a path inside the files directory ("//", "." and + * ".." collapsed, aliases of the directory mapped onto {@link #filesRoot()}), + * or null when the path is outside it. + */ + public static native String logicalPath(String path); + + public static native boolean isFile(String path); + + private static native byte[] readBytes(String path); + + private static native void nativeInit(String filesRoot, String alias); +} diff --git a/test-app/runtime/src/main/java/com/tns/Module.java b/test-app/runtime/src/main/java/com/tns/Module.java index 29317d3bd..b2958746c 100644 --- a/test-app/runtime/src/main/java/com/tns/Module.java +++ b/test-app/runtime/src/main/java/com/tns/Module.java @@ -32,7 +32,8 @@ public static void init(Logger logger, File rootPackageDir, File applicationFile } RootPackageDir = rootPackageDir.getCanonicalPath(); - ApplicationFilesPath = applicationFilesDir.getCanonicalPath(); + AppFiles.init(applicationFilesDir); + ApplicationFilesPath = AppFiles.filesRoot(); ModulesFilesPath = "/app/"; @@ -79,18 +80,32 @@ private static String resolvePath(String path, String baseDir) { } } - String resolvedPath; + return pathKey(file); + } + /* + * Inside the files directory a path is only normalized lexically: files + * there need not exist on disk, so there is nothing to canonicalize + * against. Elsewhere the real file system decides. + */ + private static String pathKey(File file) { + String absolutePath = file.getAbsolutePath(); + String logicalPath = AppFiles.logicalPath(absolutePath); + if (logicalPath != null) { + return logicalPath; + } try { - resolvedPath = file.getCanonicalPath(); + return file.getCanonicalPath(); } catch (IOException e) { if (com.tns.Runtime.isDebuggable()) { e.printStackTrace(); } - resolvedPath = file.getAbsolutePath(); + return absolutePath; } + } - return resolvedPath; + private static boolean isFile(File file) { + return AppFiles.isFile(file.getAbsolutePath()); } private static boolean isFileExternal(File source, File target) { @@ -120,8 +135,9 @@ private static File resolvePathHelper(String path, String baseDir) throws Native File possibleRelativeAbsoluteModule = prepareRelativeOrAbsoluteDir(path, baseDir); //cache - if (folderAsModuleCache.containsKey(possibleRelativeAbsoluteModule.getCanonicalPath())) { - return new File(folderAsModuleCache.get(possibleRelativeAbsoluteModule.getCanonicalPath())); + String cached = folderAsModuleCache.get(pathKey(possibleRelativeAbsoluteModule)); + if (cached != null) { + return new File(cached); } if (possibleRelativeAbsoluteModule != null) { @@ -134,8 +150,9 @@ private static File resolvePathHelper(String path, String baseDir) throws Native File possibleNativeModule = new File(NativeScriptModulesFilesPath, path); //cache - if (folderAsModuleCache.containsKey(possibleNativeModule.getCanonicalPath())) { - return new File(folderAsModuleCache.get(possibleNativeModule.getCanonicalPath())); + String cached = folderAsModuleCache.get(pathKey(possibleNativeModule)); + if (cached != null) { + return new File(cached); } foundModule = resolveFromFileOrDirectory(NativeScriptModulesFilesPath, path, possibleNativeModule); @@ -149,8 +166,9 @@ private static File resolvePathHelper(String path, String baseDir) throws Native File possibleRelativeModule = new File(possibleDir, path); //cache - if (folderAsModuleCache.containsKey(possibleRelativeModule.getCanonicalPath())) { - return new File(folderAsModuleCache.get(possibleRelativeModule.getCanonicalPath())); + String cachedModule = folderAsModuleCache.get(pathKey(possibleRelativeModule)); + if (cachedModule != null) { + return new File(cachedModule); } foundModule = resolveFromFileOrDirectory(possibleDir, path, possibleRelativeModule); @@ -202,7 +220,7 @@ private static File resolveFromFileOrDirectory(String baseDir, String path, File //cache resolved directory if (foundModule != null) { - folderAsModuleCache.put(fileOrDirectory.getCanonicalPath(), foundModule.getCanonicalPath()); + folderAsModuleCache.put(pathKey(fileOrDirectory), pathKey(foundModule)); return foundModule; } @@ -219,35 +237,19 @@ private static File loadAsFile(File path) { if (isJSFile || isMJSFile || isJSONFile || isSOFile) { // File already has an extension, try as-is - try { - File canonicalFile = path.getCanonicalFile(); - if (canonicalFile.exists() && canonicalFile.isFile()) { - return path; - } - } catch (IOException e) { - // continue to try with extensions + if (isFile(path)) { + return path; } } else { // No extension provided, try .js first, then .mjs File jsFile = new File(path.getAbsolutePath() + ".js"); - try { - File canonicalFile = jsFile.getCanonicalFile(); - if (canonicalFile.exists() && canonicalFile.isFile()) { - return jsFile; - } - } catch (IOException e) { - // continue to try .mjs + if (isFile(jsFile)) { + return jsFile; } - - // Try .mjs extension + File mjsFile = new File(path.getAbsolutePath() + ".mjs"); - try { - File canonicalFile = mjsFile.getCanonicalFile(); - if (canonicalFile.exists() && canonicalFile.isFile()) { - return mjsFile; - } - } catch (IOException e) { - // return null + if (isFile(mjsFile)) { + return mjsFile; } } @@ -262,17 +264,16 @@ private static File loadAsDirectory(String baseDir, String currentPath, File pat File packageFile = new File(path, "/package.json"); File foundFile = null; - if (packageFile.exists()) { + String packageText = AppFiles.readText(packageFile.getAbsolutePath()); + if (packageText != null) { try { - JSONObject object = FileSystem.readJSONFile(packageFile); - if (object != null) { - String mainFile = object.getString("main"); - - File pathFromMain = new File(path, mainFile); - foundFile = resolveFromFileOrDirectory(baseDir, currentPath, pathFromMain); - if (foundFile != null) { - return foundFile; - } + JSONObject object = new JSONObject(packageText); + String mainFile = object.getString("main"); + + File pathFromMain = new File(path, mainFile); + foundFile = resolveFromFileOrDirectory(baseDir, currentPath, pathFromMain); + if (foundFile != null) { + return foundFile; } } catch (IOException e) { throw new NativeScriptException(e.getMessage()); @@ -285,22 +286,14 @@ private static File loadAsDirectory(String baseDir, String currentPath, File pat //fallback to index.js foundFile = new File(path, "index.js"); - try { - if (foundFile.getCanonicalFile().exists()) { - return foundFile; - } - } catch (IOException e) { - // continue to try index.mjs + if (isFile(foundFile)) { + return foundFile; } //fallback to index.mjs foundFile = new File(path, "index.mjs"); - try { - if (foundFile.getCanonicalFile().exists()) { - return foundFile; - } - } catch (IOException e) { - return null; + if (isFile(foundFile)) { + return foundFile; } //TODO: plamen5kov: add later if necessary diff --git a/test-app/runtime/src/main/java/com/tns/Runtime.java b/test-app/runtime/src/main/java/com/tns/Runtime.java index 940a1997f..14f55849b 100644 --- a/test-app/runtime/src/main/java/com/tns/Runtime.java +++ b/test-app/runtime/src/main/java/com/tns/Runtime.java @@ -824,7 +824,7 @@ public Object runScript(File jsFile) throws NativeScriptException { public Object runScript(File jsFile, final boolean waitForResultOnMainThread) throws NativeScriptException { Object result = null; - if (jsFile.exists() && jsFile.isFile()) { + if (AppFiles.isFile(jsFile.getAbsolutePath())) { final String filePath = jsFile.getAbsolutePath(); boolean isWorkThread = threadScheduler.getThread().equals(Thread.currentThread()); From 80002698bc130a54a9b701853eed6988db200ebc Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Wed, 11 Nov 2026 14:35:07 -0300 Subject: [PATCH 4/8] feat: code cache in code_cache/v8 Move V8 code caches out of the app directory, from