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 adf8b32bed3f19..cd2a0b5f97f80b 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/sequential/sequential.status b/test/sequential/sequential.status index f4046c387a2457..0cbcd5a04b966d 100644 --- a/test/sequential/sequential.status +++ b/test/sequential/sequential.status @@ -49,6 +49,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..806b7768f0a576 --- /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'), + } + }, + { } + ); +}