diff --git a/src/services/modules/upgrade/upgrade-assert.services.ts b/src/services/modules/upgrade/upgrade-assert.services.ts index f617c6e0..e03c966c 100644 --- a/src/services/modules/upgrade/upgrade-assert.services.ts +++ b/src/services/modules/upgrade/upgrade-assert.services.ts @@ -1,31 +1,9 @@ -import {isNullish, nonNullish} from '@dfinity/utils'; -import {satelliteBuildType, type BuildType, type SatelliteParameters} from '@junobuild/admin'; -import {gunzipFile, isGzip} from '@junobuild/cli-tools'; +import {isNullish} from '@dfinity/utils'; +import {satelliteBuildType, type SatelliteParameters} from '@junobuild/admin'; import {cyan, yellow} from 'kleur'; import type {AssertWasmModule, UpgradeWasm} from '../../../types/upgrade'; import {NEW_CMD_LINE, confirmAndExit} from '../../../utils/prompt.utils'; - -const wasmBuildType = async ({wasmModule}: AssertWasmModule): Promise => { - const buffer = Buffer.from(wasmModule); - - const wasm = isGzip(buffer) - ? await gunzipFile({ - source: buffer - }) - : buffer; - - const mod = new WebAssembly.Module(wasm); - - const metadata = WebAssembly.Module.customSections(mod, 'icp:public juno:build'); - - const decoder = new TextDecoder(); - const buildType = decoder.decode(metadata[0]); - - return nonNullish(buildType) && ['stock', 'extended'].includes(buildType) - ? // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - (buildType as BuildType) - : undefined; -}; +import {readWasmModuleMetadata} from '../../../utils/wasm.utils'; export const assertSatelliteBuildType = async ({ satellite, @@ -36,8 +14,8 @@ export const assertSatelliteBuildType = async ({ const hideAgentJsConsoleWarn = globalThis.console.warn; globalThis.console.warn = (): null => null; - const [wasmTypeResult, satelliteTypeResult] = await Promise.allSettled([ - wasmBuildType({wasmModule}), + const [wasmMetadataResult, satelliteTypeResult] = await Promise.allSettled([ + readWasmModuleMetadata({wasmModule}), satelliteBuildType({ satellite }) @@ -46,7 +24,7 @@ export const assertSatelliteBuildType = async ({ // Redo console.warn globalThis.console.warn = hideAgentJsConsoleWarn; - if (wasmTypeResult.status === 'rejected') { + if (wasmMetadataResult.status === 'rejected') { throw new Error(`The custom sections of the WASM module you try to upgrade cannot be read.`); } @@ -55,9 +33,11 @@ export const assertSatelliteBuildType = async ({ return; } - const {value: wasmType} = wasmTypeResult; + const {value: wasmMetadata} = wasmMetadataResult; const {value: satelliteType} = satelliteTypeResult; + const {buildType: wasmType} = wasmMetadata; + if (satelliteType === 'extended' && (wasmType === 'stock' || isNullish(wasmType))) { await confirmAndExit( `Your satellite is currently running on an ${cyan( diff --git a/src/utils/wasm.utils.ts b/src/utils/wasm.utils.ts new file mode 100644 index 00000000..bb5bb85d --- /dev/null +++ b/src/utils/wasm.utils.ts @@ -0,0 +1,114 @@ +import {isNullish, nonNullish} from '@dfinity/utils'; +import {type BuildType, findJunoPackageDependency} from '@junobuild/admin'; +import {gunzipFile, isGzip} from '@junobuild/cli-tools'; +import {JUNO_PACKAGE_SATELLITE_ID, type JunoPackage, JunoPackageSchema} from '@junobuild/config'; +import {readFile} from 'node:fs/promises'; +import {uint8ArrayToString} from 'uint8array-extras'; + +interface WasmMetadata { + gzipped: boolean; + junoPackage: JunoPackage | undefined; + buildType: BuildType | undefined; +} + +export const readWasmFileMetadata = async ({path}: {path: string}): Promise => { + const buffer = await readFile(path); + return await readWasmMetadata({buffer}); +}; + +export const readWasmModuleMetadata = async ({ + wasmModule +}: { + wasmModule: Uint8Array; +}): Promise => { + const buffer = Buffer.from(wasmModule); + return await readWasmMetadata({buffer}); +}; + +const readWasmMetadata = async ({buffer}: {buffer: Buffer}): Promise => { + const gzipped = isGzip(buffer); + + const wasm = gzipped + ? await gunzipFile({ + source: buffer + }) + : buffer; + + const junoPackage = await readCustomSectionJunoPackage({wasm}); + + const buildType = await extractBuildType({wasm, junoPackage}); + + return { + gzipped, + junoPackage, + buildType + }; +}; + +const extractBuildType = async ({ + junoPackage, + wasm +}: { + junoPackage: JunoPackage | undefined; + wasm: Buffer; +}): Promise => { + if (isNullish(junoPackage)) { + return await readDeprecatedBuildType({wasm}); + } + + const {name, dependencies} = junoPackage; + + if (name === JUNO_PACKAGE_SATELLITE_ID) { + return 'stock'; + } + + const satelliteDependency = findJunoPackageDependency({ + dependencies, + dependencyId: JUNO_PACKAGE_SATELLITE_ID + }); + + return nonNullish(satelliteDependency) ? 'extended' : undefined; +}; + +/** + * @deprecated Modern WASM build use JunoPackage. + */ +const readDeprecatedBuildType = async ({wasm}: {wasm: Buffer}): Promise => { + const buildType = await customSection({wasm, sectionName: 'icp:public juno:build'}); + + return nonNullish(buildType) && ['stock', 'extended'].includes(buildType) + ? // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + (buildType as BuildType) + : undefined; +}; + +const readCustomSectionJunoPackage = async ({ + wasm +}: { + wasm: Buffer; +}): Promise => { + const section = await customSection({wasm, sectionName: 'icp:public juno:package'}); + + if (isNullish(section)) { + return undefined; + } + + const {success, data} = JunoPackageSchema.safeParse(section); + return success ? data : undefined; +}; + +const customSection = async ({ + sectionName, + wasm +}: { + sectionName: string; + wasm: Buffer; +}): Promise => { + const wasmModule = await WebAssembly.compile(wasm); + + const pkgSections = WebAssembly.Module.customSections(wasmModule, sectionName); + + const [pkgBuffer] = pkgSections; + + return nonNullish(pkgBuffer) ? uint8ArrayToString(pkgBuffer) : undefined; +};