diff --git a/app/build/fetch-deps.js b/app/build/fetch-deps.js index 1a52028..8460b06 100644 --- a/app/build/fetch-deps.js +++ b/app/build/fetch-deps.js @@ -39,22 +39,26 @@ const PRUNE = [ "include", "share", "Scripts", "libs", "tcl", "lib/pkgconfig", "lib/itcl4", "lib/tcl8", "lib/tcl8.6", "lib/tk8.6", "lib/thread2.8", "bin/idle3", "bin/idle3.12", "bin/pip", "bin/pip3", "bin/pip3.12", "bin/pydoc3", "bin/pydoc3.12", "bin/2to3", "bin/2to3-3.12", "bin/python3-config", "bin/python3.12-config", - ...["test", "idlelib", "tkinter", "turtledemo", "ensurepip", "lib2to3", "site-packages/pip"] + ...["test", "idlelib", "tkinter", "turtledemo", "ensurepip", "lib2to3", "site-packages/pip", "pydoc_data", "venv"] .flatMap((d) => [`lib/python3.12/${d}`, `Lib/${d}`]), ]; -function get(url) { +function get(url, redirects = 5) { return new Promise((resolve, reject) => { - https.get(url, (res) => { + https.get(url, { timeout: 60000 }, (res) => { if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { res.resume(); - return resolve(get(res.headers.location)); + if (!redirects) return reject(new Error(`${url}: too many redirects`)); + let next; + try { next = new URL(res.headers.location, url).href; } + catch { return reject(new Error(`${url}: bad redirect ${res.headers.location}`)); } + return resolve(get(next, redirects - 1)); } if (res.statusCode !== 200) return reject(new Error(`${url}: HTTP ${res.statusCode}`)); const chunks = []; res.on("data", (c) => chunks.push(c)); res.on("end", () => resolve(Buffer.concat(chunks))); - }).on("error", reject); + }).on("timeout", function () { this.destroy(new Error(`${url}: timed out`)); }).on("error", reject); }); } @@ -66,7 +70,7 @@ async function download(url, sha256, file) { } // Windows' own bsdtar: Git's GNU tar, often first on PATH, reads C:\ as a remote host. -const TAR = process.platform === "win32" ? path.join(process.env.SystemRoot, "System32", "tar.exe") : "tar"; +const TAR = process.platform === "win32" ? path.join(process.env.SystemRoot || "C:\\Windows", "System32", "tar.exe") : "tar"; function extract(file, dir) { fs.mkdirSync(dir, { recursive: true }); @@ -97,6 +101,12 @@ async function fetch(os, arch) { await download(PY_URL(triple), pySha, tgz); extract(tgz, out); // unpacks to python/ for (const p of PRUNE) fs.rmSync(path.join(out, "python", p), { recursive: true, force: true }); + const stdlib = path.join(out, "python", "lib", "python3.12"); // macOS and Linux: drop the static libpython + if (fs.existsSync(stdlib)) { + for (const d of fs.readdirSync(stdlib)) { + if (d.startsWith("config-3.12")) fs.rmSync(path.join(stdlib, d), { recursive: true, force: true }); + } + } const tools = path.join(out, "tools"); fs.mkdirSync(tools); diff --git a/app/main.js b/app/main.js index 230b2e6..ad64d8f 100644 --- a/app/main.js +++ b/app/main.js @@ -236,8 +236,10 @@ async function firstRunCheck() { // IPC only from our own page in our own window. function fromUi(e) { - return !!(win && e.sender === win.webContents && url && e.senderFrame - && new URL(e.senderFrame.url).origin === new URL(url).origin); + if (!win || e.sender !== win.webContents || !url || !e.senderFrame) return false; + try { + return new URL(e.senderFrame.url).origin === new URL(url).origin; + } catch { return false; } } ipcMain.handle("clipboard:read", (e) => fromUi(e) ? clipboard.readText() : ""); diff --git a/docs/frame-control.md b/docs/frame-control.md index e0e9922..0dfb4ae 100644 --- a/docs/frame-control.md +++ b/docs/frame-control.md @@ -124,7 +124,9 @@ The arm64 build also needs your distribution's `adb` for Android apps, because Google publishes no arm64 Linux platform-tools. Set Up Connection runs `ui/frame_connect.py` in your terminal emulator (GNOME Terminal, Konsole, xterm and others). The log is at -`~/.config/Frame Control/logs/server.log`. +`~/.config/Frame Control/logs/server.log`. Running `ui/server.py` in a browser +instead of the app, sending the clipboard needs `wl-clipboard` (Wayland) or +`xclip` (X11). ## Building diff --git a/tests/test_frame_apk.py b/tests/test_frame_apk.py index 315b4c0..f9c008b 100644 --- a/tests/test_frame_apk.py +++ b/tests/test_frame_apk.py @@ -27,10 +27,14 @@ def pool(strings, utf8=False): return struct.pack('.""" +def manifest(package, label_ref, version_ref, min_sdk, package_raw=True, foreign_label=False): + """. + + package_raw=False drops the package's raw string (as some repackers do); + foreign_label adds a non-android `label` attribute after android:label. + """ strings = ['label', 'icon', 'versionName', 'minSdkVersion', 'package', 'manifest', 'uses-sdk', - 'application', package] + 'application', package, 'junk', 'label'] # the second 'label' has no android id resmap = struct.pack('<4I', 0x01010001, 0x01010002, 0x0101021c, 0x0101020c) resmap = struct.pack(''})): with self.assertRaises(frame_apk.ApkError): diff --git a/ui/frame_apk.py b/ui/frame_apk.py index 9c7b5bc..53fd58a 100644 --- a/ui/frame_apk.py +++ b/ui/frame_apk.py @@ -71,9 +71,14 @@ def manifest_elements(data): for i in range(count): a = off + hsize + astart + i * asize aname, raw, dtype, value = struct.unpack_from('<4xII3xBI', data, a) - key = ATTR.get(resmap[aname]) if aname < len(resmap) else None - key = key or (strings[aname] if aname < len(strings) else '') - attrs[key] = (dtype, value, strings[raw] if raw < len(strings) else None) + raw = strings[raw] if raw < len(strings) else None + if raw is None and dtype == T_STRING and value < len(strings): + raw = strings[value] # some repackers keep only the typed value + android = ATTR.get(resmap[aname]) if aname < len(resmap) else None + if android: # android: attributes win over same-named ones in other namespaces + attrs[android] = (dtype, value, raw) + else: + attrs.setdefault(strings[aname] if aname < len(strings) else '', (dtype, value, raw)) out.append((strings[name] if name < len(strings) else '', attrs)) return out @@ -197,18 +202,23 @@ def apk_info(path): 'min_sdk': min_sdk[1] if min_sdk and min_sdk[0] in (T_INT_DEC, T_INT_HEX) else None, 'icon_png': None, } - for icon in _icons(app.get('icon'), res): - if icon.endswith('.png') and icon in names: - info['icon_png'] = z.read(icon) - break - else: # adaptive icons are XML; fall back to the largest launcher PNG - pngs = sorted((n for n in names if n.endswith('.png') and 'ic_launcher' in n and 'foreground' not in n), - key=lambda n: z.getinfo(n).file_size) - if pngs: - info['icon_png'] = z.read(pngs[-1]) + try: + info['icon_png'] = _icon_png(z, names, _icons(app.get('icon'), res)) + except Exception: # noqa: BLE001 - any unreadable icon just means no icon + pass return info +def _icon_png(z, names, icons): + for icon in icons: + if icon.endswith('.png') and icon in names: + return z.read(icon) + # Adaptive icons are XML; fall back to the largest launcher PNG. + pngs = sorted((n for n in names if n.endswith('.png') and 'ic_launcher' in n and 'foreground' not in n), + key=lambda n: z.getinfo(n).file_size) + return z.read(pngs[-1]) if pngs else None + + if __name__ == '__main__': import sys for p in sys.argv[1:]: