From eea0d74454a86c27a4b840132b5169b0b7783573 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Tue, 28 Nov 2023 18:58:15 +0100 Subject: [PATCH] sea: support sea.getRawAsset() This patch adds support for `sea.getRawAsset()` which is similar to `sea.getAsset()` but returns the raw asset in an array buffer without copying. Users should avoid writing to the returned array buffer. If the injected section is not marked as writable or not aligned, writing to the raw asset is likely to result in a crash. PR-URL: https://github.com/nodejs/node/pull/50960 Refs: https://github.com/nodejs/single-executable/issues/68 Reviewed-By: Antoine du Hamel Reviewed-By: Stephen Belanger --- doc/api/single-executable-applications.md | 23 ++++++ lib/sea.js | 1 + test/fixtures/sea/get-asset-raw.js | 31 ++++++++ test/fixtures/sea/get-asset.js | 2 + test/sequential/sequential.status | 1 + ...ingle-executable-application-assets-raw.js | 73 +++++++++++++++++++ 6 files changed, 131 insertions(+) create mode 100644 test/fixtures/sea/get-asset-raw.js create mode 100644 test/sequential/test-single-executable-application-assets-raw.js diff --git a/doc/api/single-executable-applications.md b/doc/api/single-executable-applications.md index ccaa938b6c1d33..530fbff74ae553 100644 --- a/doc/api/single-executable-applications.md +++ b/doc/api/single-executable-applications.md @@ -219,6 +219,8 @@ const image = getAsset('a.jpg'); const text = getAsset('b.txt', 'utf8'); // Returns a Blob containing the asset. const blob = getAssetAsBlob('a.jpg'); +// Returns an ArrayBuffer containing the raw asset without copying. +const raw = getRawAsset('a.jpg'); ``` See documentation of the [`sea.getAsset()`][] and [`sea.getAssetAsBlob()`][] @@ -316,6 +318,27 @@ An error is thrown when no matching asset can be found. * `type` {string} An optional mime type for the blob. * Returns: {Blob} +### `sea.getRawAsset(key)` + + + +This method can be used to retrieve the assets configured to be bundled into the +single-executable application at build time. +An error is thrown when no matching asset can be found. + +Unlike `sea.getRawAsset()` or `sea.getAssetAsBlob()`, this method does not +return a copy. Instead, it returns the raw asset bundled inside the executable. + +For now, users should avoid writing to the returned array buffer. If the +injected section is not marked as writable or not aligned properly, +writes to the returned array buffer is likely to result in a crash. + +* `key` {string} the key for the asset in the dictionary specified by the + `assets` field in the single-executable application configuration. +* Returns: {string|ArrayBuffer} + ### `require(id)` in the injected main script is not file based `require()` in the injected main script is not the same as the [`require()`][] diff --git a/lib/sea.js b/lib/sea.js index e23f29724cee2a..f7727014c4e3c9 100644 --- a/lib/sea.js +++ b/lib/sea.js @@ -71,5 +71,6 @@ function getAssetAsBlob(key, options) { module.exports = { isSea, getAsset, + getRawAsset, getAssetAsBlob, }; diff --git a/test/fixtures/sea/get-asset-raw.js b/test/fixtures/sea/get-asset-raw.js new file mode 100644 index 00000000000000..0ba9858c01109e --- /dev/null +++ b/test/fixtures/sea/get-asset-raw.js @@ -0,0 +1,31 @@ +'use strict'; + +const { isSea, getAsset, getRawAsset } = require('node:sea'); +const { readFileSync } = require('fs'); +const assert = require('assert'); + +assert(isSea()); + +{ + assert.throws(() => getRawAsset('nonexistent'), { + code: 'ERR_SINGLE_EXECUTABLE_APPLICATION_ASSET_NOT_FOUND' + }); + assert.throws(() => getRawAsset(null), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => getRawAsset(1), { + code: 'ERR_INVALID_ARG_TYPE' + }); +} + +{ + // Check that the asset embedded is the same as the original. + const assetOnDisk = readFileSync(process.env.__TEST_PERSON_JPG); + const assetCopy = getAsset('person.jpg') + const assetCopyBuffer = Buffer.from(assetCopy); + assert.deepStrictEqual(assetCopyBuffer, assetOnDisk); + + // Check that the copied asset is the same as the raw one. + const rawAsset = getRawAsset('person.jpg'); + assert.deepStrictEqual(rawAsset, assetCopy); +} diff --git a/test/fixtures/sea/get-asset.js b/test/fixtures/sea/get-asset.js index b5dc83518b2279..e1a2189aa4da26 100644 --- a/test/fixtures/sea/get-asset.js +++ b/test/fixtures/sea/get-asset.js @@ -77,6 +77,8 @@ const binaryAssetOnDisk = readFileSync(process.env.__TEST_PERSON_JPG); { const actualAsset = getAsset('utf8_test_text.txt', 'utf8') assert.strictEqual(actualAsset, textAssetOnDisk); + // Log it out so that the test could compare it and see if + // it's encoded/decoded correctly in the SEA. console.log(actualAsset); } diff --git a/test/sequential/sequential.status b/test/sequential/sequential.status index 8f38cead1adfee..ccab879b6e5fcd 100644 --- a/test/sequential/sequential.status +++ b/test/sequential/sequential.status @@ -51,6 +51,7 @@ test-performance-eventloopdelay: PASS, FLAKY [$system==linux && $arch==ppc64] # https://github.com/nodejs/node/issues/50740 +test-single-executable-application-assets-raw: PASS, FLAKY test-single-executable-application-assets: PASS, FLAKY test-single-executable-application-disable-experimental-sea-warning: PASS, FLAKY test-single-executable-application-empty: PASS, FLAKY diff --git a/test/sequential/test-single-executable-application-assets-raw.js b/test/sequential/test-single-executable-application-assets-raw.js new file mode 100644 index 00000000000000..6f0a8a77486fb6 --- /dev/null +++ b/test/sequential/test-single-executable-application-assets-raw.js @@ -0,0 +1,73 @@ +'use strict'; + +const common = require('../common'); + +const { + injectAndCodeSign, + skipIfSingleExecutableIsNotSupported, +} = require('../common/sea'); + +skipIfSingleExecutableIsNotSupported(); + +// This tests the snapshot support in single executable applications. +const tmpdir = require('../common/tmpdir'); + +const { copyFileSync, writeFileSync, existsSync } = require('fs'); +const { + spawnSyncAndExitWithoutError, +} = require('../common/child_process'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); + +tmpdir.refresh(); +if (!tmpdir.hasEnoughSpace(120 * 1024 * 1024)) { + common.skip('Not enough disk space'); +} + +const configFile = tmpdir.resolve('sea-config.json'); +const seaPrepBlob = tmpdir.resolve('sea-prep.blob'); +const outputFile = tmpdir.resolve(process.platform === 'win32' ? 'sea.exe' : 'sea'); + +{ + tmpdir.refresh(); + copyFileSync(fixtures.path('sea', 'get-asset-raw.js'), tmpdir.resolve('sea.js')); + copyFileSync(fixtures.path('person.jpg'), tmpdir.resolve('person.jpg')); + writeFileSync(configFile, ` + { + "main": "sea.js", + "output": "sea-prep.blob", + "assets": { + "person.jpg": "person.jpg" + } + } + `, 'utf8'); + + spawnSyncAndExitWithoutError( + process.execPath, + ['--experimental-sea-config', 'sea-config.json'], + { + env: { + NODE_DEBUG_NATIVE: 'SEA', + ...process.env, + }, + cwd: tmpdir.path + }, + {}); + + assert(existsSync(seaPrepBlob)); + + copyFileSync(process.execPath, outputFile); + injectAndCodeSign(outputFile, seaPrepBlob); + + spawnSyncAndExitWithoutError( + outputFile, + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'SEA', + __TEST_PERSON_JPG: fixtures.path('person.jpg'), + } + }, + { } + ); +}