| … | … | @@ -0,0 +1,126 @@ |
| 1 | // The loader half of OTA-delivered native addons. |
| 2 | // |
| 3 | // ⛔ WHY A SEPARATE MODULE FROM THE STAGED BUNDLE. worklet.js hands bare-kit a PATH and a null source, |
| 4 | // so Bare loads the staged bundle as its own entry (NativeBareKit.startFile). Preload code placed |
| 5 | // inside that bundle is too late and, worse, circular: its own `import` of bare-fs resolves to |
| 6 | // `linked:libbare-fs.<staged version>.so` — the very soname the APK might not have. The loader must |
| 7 | // therefore be its own bundle, packed against the APK's addon set, which then registers the staged |
| 8 | // addons and loads the real bundle. |
| 9 | // |
| 10 | // ⛔ AND WHY THE FILENAME IS THE CONTRACT. Addon.cache is keyed by url.href and the `linked:` branch of |
| 11 | // Addon.resolve goes through Addon.load, so registering under `linked:<soname>` wins before any |
| 12 | // dlopen-by-soname. Naming each staged file after the soname it satisfies means the loader needs no |
| 13 | // manifest and never parses the bundle — the directory listing IS the mapping. |
| 14 | import test from 'brittle' |
| 15 | import fs from 'bare-fs' |
| 16 | import path from 'bare-path' |
| 17 | import os from 'bare-os' |
| 18 | import { fileURLToPath } from 'bare-url' |
| 19 | import { addonHref, preloadAddons } from '../lib/host/preload-addons.js' |
| 20 | |
| 21 | let seq = 0 |
| 22 | function tmpdir (name) { |
| 23 | const dir = path.join(os.tmpdir(), 'favela-preload-' + name + '-' + (++seq)) |
| 24 | fs.rmSync(dir, { recursive: true, force: true }) |
| 25 | fs.mkdirSync(dir, { recursive: true }) |
| 26 | return dir |
| 27 | } |
| 28 | |
| 29 | test('a staged filename maps to the linked: URL the bundle asks for', (t) => { |
| 30 | t.is(addonHref('libbare-fs.4.8.1.so'), 'linked:libbare-fs.4.8.1.so') |
| 31 | t.is(addonHref('libsodium-native.5.1.0.so'), 'linked:libsodium-native.5.1.0.so') |
| 32 | }) |
| 33 | |
| 34 | test('every staged addon is registered under its linked: URL', (t) => { |
| 35 | const dir = tmpdir('register') |
| 36 | fs.writeFileSync(path.join(dir, 'libbare-fs.4.8.1.so'), 'x') |
| 37 | fs.writeFileSync(path.join(dir, 'libsodium-native.5.1.0.so'), 'x') |
| 38 | |
| 39 | const cache = Object.create(null) |
| 40 | const loaded = [] |
| 41 | const Addon = function (url) { loaded.push(url.href); this.url = url } |
| 42 | const n = preloadAddons({ dir, cache, Addon }) |
| 43 | |
| 44 | t.is(n, 2, 'both registered') |
| 45 | t.ok(cache['linked:libbare-fs.4.8.1.so'], 'bare-fs is in the cache under its soname') |
| 46 | t.ok(cache['linked:libsodium-native.5.1.0.so'], 'sodium-native too') |
| 47 | t.ok(loaded.every((h) => h.startsWith('file://')), 'each was loaded from an absolute file: path') |
| 48 | t.ok(loaded.some((h) => h.endsWith('/libbare-fs.4.8.1.so')), 'from the staged directory') |
| 49 | }) |
| 50 | |
| 51 | test('an addon the APK already satisfies is NOT overridden if it is not staged', (t) => { |
| 52 | // Staging is additive. Anything absent from the dir must fall through to dlopen-by-soname, which is |
| 53 | // what makes a release able to ship ONE changed addon rather than all 58. |
| 54 | const dir = tmpdir('additive') |
| 55 | fs.writeFileSync(path.join(dir, 'libbare-fs.4.8.1.so'), 'x') |
| 56 | |
| 57 | const cache = Object.create(null) |
| 58 | preloadAddons({ dir, cache, Addon: function (url) { this.url = url } }) |
| 59 | |
| 60 | t.ok(cache['linked:libbare-fs.4.8.1.so'], 'the staged one is registered') |
| 61 | t.absent(cache['linked:libsodium-native.5.1.0.so'], 'the unstaged one is left to the APK') |
| 62 | }) |
| 63 | |
| 64 | test('no addons directory is zero, not a throw', (t) => { |
| 65 | // Every release published so far carries no addons at all, and the loader runs on every boot. |
| 66 | const cache = Object.create(null) |
| 67 | t.is(preloadAddons({ dir: null, cache, Addon: function () {} }), 0, 'null dir') |
| 68 | t.is(preloadAddons({ dir: path.join(os.tmpdir(), 'does-not-exist-' + (++seq)), cache, Addon: function () {} }), 0, 'missing dir') |
| 69 | t.alike(cache, Object.create(null), 'nothing registered') |
| 70 | }) |
| 71 | |
| 72 | test('a non-library file in the directory is ignored', (t) => { |
| 73 | const dir = tmpdir('junk') |
| 74 | fs.writeFileSync(path.join(dir, 'libbare-fs.4.8.1.so'), 'x') |
| 75 | fs.writeFileSync(path.join(dir, 'README'), 'x') |
| 76 | fs.writeFileSync(path.join(dir, 'libbroken.so.part'), 'x') // an interrupted write |
| 77 | |
| 78 | const cache = Object.create(null) |
| 79 | const n = preloadAddons({ dir, cache, Addon: function (url) { this.url = url } }) |
| 80 | t.is(n, 1, 'only the .so is taken') |
| 81 | t.absent(cache['linked:README'], 'no junk in the cache') |
| 82 | t.absent(cache['linked:libbroken.so.part'], 'a .part is not a library') |
| 83 | }) |
| 84 | |
| 85 | test('one addon that fails to load does not stop the others', (t) => { |
| 86 | // A bundle missing ONE addon still cannot run — but the loader is not the place to decide that, and |
| 87 | // dying here would take out the boot with no diagnostic. Report and continue; the failure surfaces |
| 88 | // where it means something, at the require that needs it. |
| 89 | const dir = tmpdir('partial') |
| 90 | fs.writeFileSync(path.join(dir, 'liba.1.0.0.so'), 'x') |
| 91 | fs.writeFileSync(path.join(dir, 'libb.1.0.0.so'), 'x') |
| 92 | |
| 93 | const cache = Object.create(null) |
| 94 | const said = [] |
| 95 | const n = preloadAddons({ |
| 96 | dir, |
| 97 | cache, |
| 98 | Addon: function (url) { if (url.href.endsWith('liba.1.0.0.so')) throw new Error('bad ELF'); this.url = url }, |
| 99 | say: (m) => said.push(m) |
| 100 | }) |
| 101 | t.is(n, 1, 'the good one is still registered') |
| 102 | t.ok(cache['linked:libb.1.0.0.so'], 'b loaded') |
| 103 | t.absent(cache['linked:liba.1.0.0.so'], 'a did not') |
| 104 | t.ok(said.some((m) => m.includes('liba.1.0.0.so') && m.includes('bad ELF')), 'and it said which and why: ' + said.join(' | ')) |
| 105 | }) |
| 106 | |
| 107 | test('REAL: a genuine addon loads from an arbitrary staged path', (t) => { |
| 108 | // The whole route in one assertion, against Bare's real Addon rather than a double. A prebuild copied |
| 109 | // under a new name is a path the linker has never seen, which is exactly the OTA case: bytes that |
| 110 | // arrived over the swarm, sitting in app storage, loaded by absolute path. |
| 111 | const here = path.dirname(fileURLToPath(import.meta.url)) |
| 112 | const src = path.join(here, '..', 'node_modules', 'bare-url', 'prebuilds', Bare.Addon.host, 'bare-url.bare') |
| 113 | if (!fs.existsSync(src)) return t.pass('no prebuild for ' + Bare.Addon.host + ' — nothing to prove here') |
| 114 | |
| 115 | const dir = tmpdir('real') |
| 116 | const staged = path.join(dir, 'libstaged-probe.9.9.9.so') |
| 117 | fs.writeFileSync(staged, fs.readFileSync(src)) |
| 118 | fs.chmodSync(staged, 0o755) |
| 119 | |
| 120 | const cache = Object.create(null) |
| 121 | const n = preloadAddons({ dir, cache }) |
| 122 | t.is(n, 1, 'loaded') |
| 123 | const addon = cache['linked:libstaged-probe.9.9.9.so'] |
| 124 | t.ok(addon, 'registered under the soname a bundle would ask for') |
| 125 | t.ok(addon.exports && typeof addon.exports === 'object', 'and it really initialised — it has exports') |
| 126 | }) |