#pragma once // .fenv — a Frame environment in one file: the home's APKs (stored, not recompressed: they are already zips), // Frame Studio's object edits, imported models (glTF / GLB / FBX / OBJ from Blender or Unity, with the texture // files they reference) and a thumbnail, described by fenv.json: // // { "format": "fenv", "version": 1, "name": "Haven + Calming", // "sources": [ { "file": "apks/haven2025.apk", "role": "Footprint", "format": "HSL", // "package": "com.meta.shell.env.footprint.haven2025", "version": "206.0.0.0.70" } ], // "models": [ { "file": "models/chair/chair.glb" } ], // "edits": "edits.json", "thumbnail": "thumb.png" } // // Frame Studio writes and opens it; Frame Home and the switcher on the Steam Frame load it directly. Opening // extracts the members once into a cache folder keyed by the file's path, size and time, then loads them from there. // A member named ".lz" is an LZMA stream (fenv_lzma.h) and is unpacked to — the baked scene is stored // so ("scene.bin.lz"): the zip's own deflate is far weaker on ASTC textures. #include "core/tinyjson.h" #include "world_settings.h" #ifdef _WIN32 #include #include // LockFileEx (the builds define NOMINMAX / WIN32_LEAN_AND_MEAN) #else #include #include #include #endif #include "fenv_lzma.h" #include "miniz.h" #include #include #include #include #include #include #include #include #include #include #include #include struct FenvSource { std::string file, role, format, package, version; }; struct FenvManifest { std::string name; std::vector sources; // footprint first, then the vista (or one whole home) std::vector models; // imported model files (member paths) std::string edits, thumbnail; // member paths, empty when absent std::string scene; // the baked scene (scene_bake.h): when present it IS the home, sources are info std::string world; // world.json (core/world.h): spawns, seats, walls, walkable areas WorldSettings settings; // the world's own render settings (world_settings.h): none set = the headset's }; namespace fenv_detail { inline std::string quote(const std::string& s) { std::string o = "\""; for (char c : s) { if (c == '"' || c == '\\') { o += '\\'; o += c; } else if (static_cast(c) < 0x20) { char b[8]; std::snprintf(b, sizeof b, "\\u%04x", c); o += b; } else o += c; } return o + "\""; } inline std::string manifestJson(const FenvManifest& m) { std::string j = "{\n \"format\": \"fenv\",\n \"version\": 1,\n \"name\": " + quote(m.name) + ",\n \"sources\": ["; for (size_t i = 0; i < m.sources.size(); ++i) { const FenvSource& s = m.sources[i]; j += std::string(i ? "," : "") + "\n { \"file\": " + quote(s.file) + ", \"role\": " + quote(s.role) + ", \"format\": " + quote(s.format) + ", \"package\": " + quote(s.package) + ", \"version\": " + quote(s.version) + " }"; } j += "\n ],\n \"models\": ["; for (size_t i = 0; i < m.models.size(); ++i) j += std::string(i ? "," : "") + "\n { \"file\": " + quote(m.models[i]) + " }"; j += "\n ]"; if (!m.edits.empty()) j += ",\n \"edits\": " + quote(m.edits); if (!m.thumbnail.empty()) j += ",\n \"thumbnail\": " + quote(m.thumbnail); if (!m.scene.empty()) j += ",\n \"scene\": " + quote(m.scene); if (!m.world.empty()) j += ",\n \"world\": " + quote(m.world); if (!m.settings.empty() || !m.settings.tuned.empty()) j += ",\n \"settings\": " + m.settings.toJson(); return j + "\n}\n"; } // Everything one .fenv may unpack to (its members, LZMA ones as unpacked): a damaged or hostile file must not // fill the disk or memory. The largest homes are a few hundred MB. constexpr uint64_t kExtractBudget = uint64_t(4) << 30; // Empty when missing or larger than `limit` (the zip's declared size, which miniz holds the inflate to). inline std::vector readMember(mz_zip_archive& z, const char* name, uint64_t limit = uint64_t(64) << 20) { std::vector out; const int i = mz_zip_reader_locate_file(&z, name, nullptr, 0); mz_zip_archive_file_stat st; if (i < 0 || !mz_zip_reader_file_stat(&z, static_cast(i), &st) || st.m_uncomp_size > limit || st.m_comp_size > limit) return out; size_t n = 0; void* p = mz_zip_reader_extract_to_heap(&z, static_cast(i), &n, 0); if (!p) return out; try { out.assign(static_cast(p), static_cast(p) + n); } catch (const std::exception&) { out = {}; } mz_free(p); return out; } // A short tag for a file's full path: two "haven.fenv" in different folders get different cache folders. inline std::string pathTag(const std::filesystem::path& p) { std::error_code ec; std::filesystem::path full = std::filesystem::weakly_canonical(p, ec); if (ec || full.empty()) full = std::filesystem::absolute(p, ec).lexically_normal(); std::string s = full.u8string(); #ifdef _WIN32 for (char& c : s) { if (c >= 'A' && c <= 'Z') c = static_cast(c - 'A' + 'a'); else if (c == '/') c = '\\'; } // one file, however spelled #endif uint64_t h = 1469598103934665603ull; // FNV-1a for (unsigned char c : s) h = (h ^ c) * 1099511628211ull; char b[16]; std::snprintf(b, sizeof b, "%08x", static_cast(h ^ (h >> 32))); return b; } // One unpacker per cache key at a time, across processes (Frame Home and the switcher may open the same // environment together; on Windows, two Studio windows). struct ExtractLock { #ifdef _WIN32 HANDLE h = INVALID_HANDLE_VALUE; OVERLAPPED ov = {}; explicit ExtractLock(const std::filesystem::path& p) { h = CreateFileW(p.wstring().c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (h != INVALID_HANDLE_VALUE && !LockFileEx(h, LOCKFILE_EXCLUSIVE_LOCK, 0, MAXDWORD, MAXDWORD, &ov)) { CloseHandle(h); h = INVALID_HANDLE_VALUE; } } ~ExtractLock() { if (h != INVALID_HANDLE_VALUE) { UnlockFileEx(h, 0, MAXDWORD, MAXDWORD, &ov); CloseHandle(h); } } #else int fd; explicit ExtractLock(const std::filesystem::path& p) : fd(open(p.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0600)) { if (fd >= 0) flock(fd, LOCK_EX); } ~ExtractLock() { if (fd >= 0) { flock(fd, LOCK_UN); close(fd); } } #endif ExtractLock(const ExtractLock&) = delete; ExtractLock& operator=(const ExtractLock&) = delete; }; inline long processId() { #ifdef _WIN32 return static_cast(_getpid()); #else return static_cast(getpid()); #endif } // A member path is safe to extract: relative, no "..", no drive letters. inline bool safeMember(const std::string& m) { if (m.empty() || m[0] == '/' || m[0] == '\\' || m.find(':') != std::string::npos) return false; for (size_t a = 0; a <= m.size();) { size_t b = m.find_first_of("/\\", a); if (b == std::string::npos) b = m.size(); if (m.compare(a, b - a, "..") == 0 && b - a == 2) return false; a = b + 1; } return true; } } // namespace fenv_detail // Where opened .fenv files are unpacked: FRAME_FENV_CACHE, else %LOCALAPPDATA%\FrameStudio\cache (Windows) or // $XDG_CACHE_HOME / ~/.cache /frame-home/fenv. inline std::string fenvCacheDir() { if (const char* e = std::getenv("FRAME_FENV_CACHE")) return e; #ifdef _WIN32 if (const char* l = std::getenv("LOCALAPPDATA")) return std::string(l) + "\\FrameStudio\\cache"; #endif if (const char* x = std::getenv("XDG_CACHE_HOME")) return std::string(x) + "/frame-home/fenv"; const char* h = std::getenv("HOME"); return std::string(h ? h : "/tmp") + "/.cache/frame-home/fenv"; } inline bool isFenvPath(const std::string& p) { return p.size() > 5 && (p.compare(p.size() - 5, 5, ".fenv") == 0 || p.compare(p.size() - 5, 5, ".FENV") == 0); } inline bool fenvParseManifest(const std::string& json, FenvManifest& m) { try { const tinyjson::Value doc = tinyjson::parse(json); if (!doc.isObject() || doc["format"].asString() != "fenv") return false; m = FenvManifest{}; m.name = doc["name"].asString(); for (size_t i = 0; i < doc["sources"].size(); ++i) { const auto& s = doc["sources"][i]; FenvSource src; src.file = s["file"].asString(); src.role = s["role"].asString(); src.format = s["format"].asString(); src.package = s["package"].asString(); src.version = s["version"].asString(); if (fenv_detail::safeMember(src.file)) m.sources.push_back(src); } for (size_t i = 0; i < doc["models"].size(); ++i) { const std::string f = doc["models"][i]["file"].asString(); if (fenv_detail::safeMember(f)) m.models.push_back(f); } if (doc.has("edits") && fenv_detail::safeMember(doc["edits"].asString())) m.edits = doc["edits"].asString(); if (doc.has("thumbnail") && fenv_detail::safeMember(doc["thumbnail"].asString())) m.thumbnail = doc["thumbnail"].asString(); if (doc.has("scene") && fenv_detail::safeMember(doc["scene"].asString())) m.scene = doc["scene"].asString(); if (doc.has("world") && fenv_detail::safeMember(doc["world"].asString())) m.world = doc["world"].asString(); if (doc.has("settings")) m.settings.fromValue(doc["settings"]); return !m.sources.empty() || !m.models.empty() || !m.scene.empty(); } catch (...) { return false; } } // Reads fenv.json (and, if asked, the thumbnail bytes) without extracting anything else. inline bool fenvReadManifest(const std::string& path, FenvManifest& m, std::vector* thumb = nullptr) { mz_zip_archive z = {}; if (!mz_zip_reader_init_file(&z, path.c_str(), 0)) return false; const std::vector j = fenv_detail::readMember(z, "fenv.json"); bool ok = !j.empty() && fenvParseManifest(std::string(j.begin(), j.end()), m); if (ok && thumb && !m.thumbnail.empty()) *thumb = fenv_detail::readMember(z, m.thumbnail.c_str()); mz_zip_reader_end(&z); return ok; } // Extracts every member into /---