Loading files
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { invalidModSettings, makeXsArchive, modDefinition } from '../../firmware/contracts/testing/xsa-fixture.js'
import {
bytesToBinaryString,
DEVICE_OPERATION_STATUS,
findXsPartition,
findAppPartition,
installModToDevice,
equalBytes,
removeModFromDevice,
xsArchiveByteLength,
parsePartitionTable,
parseEspAppDescriptor,
PARTITION_TABLE_OFFSET,
PARTITION_TABLE_SIZE,
} from './esptool-installer.mjs'
// Build a synthetic ESP-IDF partition table image.
function makePartitionTable(entries) {
const buf = new Uint8Array(0xc00).fill(0xff)
const view = new DataView(buf.buffer)
entries.forEach((e, i) => {
const pos = i * 32
view.setUint16(pos, 0x50aa, true)
view.setUint8(pos + 2, e.type)
view.setUint8(pos + 3, e.subtype)
view.setUint32(pos + 4, e.offset, true)
view.setUint32(pos + 8, e.size, true)
for (let c = 0; c < 16; c++) view.setUint8(pos + 12 + c, 0) // real tables zero-pad the label
for (let c = 0; c < e.label.length && c < 15; c++) view.setUint8(pos + 12 + c, e.label.charCodeAt(c))
})
// md5 terminator row (magic 0xEBEB) so parsing stops
view.setUint16(entries.length * 32, 0xebeb, true)
return buf
}
const CORES3_TABLE = makePartitionTable([
{ type: 0x01, subtype: 0x02, offset: 0x9000, size: 0x6000, label: 'nvs' },
{ type: 0x01, subtype: 0x01, offset: 0xf000, size: 0x1000, label: 'phy_init' },
{ type: 0x00, subtype: 0x00, offset: 0x10000, size: 0xf90000, label: 'factory' },
{ type: 0x40, subtype: 0x01, offset: 0xfa0000, size: 0x40000, label: 'xs' },
{ type: 0x01, subtype: 0x82, offset: 0xfe0000, size: 0x10000, label: 'storage' },
])
function makeAppHeader(version = '9.5.0+stackchan.2', projectName = 'xs_esp32') {
const bytes = new Uint8Array(256)
const view = new DataView(bytes.buffer)
view.setUint32(0x20, 0xabcd5432, true)
bytes.set(new TextEncoder().encode(version), 0x30)
bytes.set(new TextEncoder().encode(projectName), 0x50)
return bytes
}
function makeArchive(size = 32) {
return makeXsArchive({ metadata: modDefinition, padding: size })
}
test('embedded API requirements are checked before user preflight or writing and cannot be overridden', async () => {
const archive = makeXsArchive()
for (const hostApi of [0, 1, 2]) {
const calls = []
const loader = {
async main() {
return 'ESP32-S3'
},
async readFlash(offset) {
return offset === PARTITION_TABLE_OFFSET
? CORES3_TABLE
: offset === 0x10000
? makeAppHeader('9.5.0' + (hostApi ? '+stackchan.' + hostApi : ''))
: archive
},
async writeFlash() {
calls.push('write')
},
async resetToRunApp() {},
async disconnect() {},
}
const operation = installModToDevice(async () => loader, {}, archive, {
onPreflight() {
calls.push('preflight')
return true
},
})
if (hostApi < modDefinition.hostApiVersion) {
await assert.rejects(operation, (error) => error.code === 'MOD_HOST_API_UNSUPPORTED')
assert.deepEqual(calls, [])
} else {
assert.equal((await operation).status, DEVICE_OPERATION_STATUS.INSTALLED)
assert.deepEqual(calls, ['preflight', 'write'])
}
}
})
test('unsupported XS and SDK versions are rejected before approval or writing', async () => {
let connected = false
await assert.rejects(
installModToDevice(
async () => {
connected = true
},
{},
makeXsArchive({ version: [99, 0, 0] })
),
/XS archive version/
)
assert.equal(connected, false)
const calls = []
const loader = {
async main() {
return 'ESP32-S3'
},
async readFlash(offset) {
return offset === PARTITION_TABLE_OFFSET ? CORES3_TABLE : makeAppHeader('8.3.1+stackchan.2')
},
async writeFlash() {
calls.push('write')
},
async disconnect() {},
}
await assert.rejects(
installModToDevice(async () => loader, {}, makeXsArchive(), {
onPreflight() {
calls.push('approve')
return true
},
}),
/Moddable 9.5/
)
assert.deepEqual(calls, [])
})
test('a corrupt declaration is rejected before connecting instead of treated as legacy', async () => {
let connected = false
await assert.rejects(
installModToDevice(
async () => {
connected = true
},
{},
makeXsArchive({ metadata: { ...modDefinition, schemaVersion: 99 } })
),
(error) => error.code === 'MOD_METADATA_INVALID'
)
assert.equal(connected, false)
})
test('parsePartitionTable reads all entries and stops at the terminator', () => {
const parts = parsePartitionTable(CORES3_TABLE)
assert.equal(parts.length, 5)
assert.equal(parts[3].label, 'xs')
assert.equal(parts[3].type, 0x40)
})
test('findXsPartition locates the mod partition by type/subtype', () => {
const xs = findXsPartition(parsePartitionTable(CORES3_TABLE))
assert.equal(xs.offset, 0xfa0000)
assert.equal(xs.size, 0x40000)
})
test('reads the factory app descriptor used for firmware/XS compatibility checks', () => {
assert.equal(findAppPartition(parsePartitionTable(CORES3_TABLE)).offset, 0x10000)
assert.deepEqual(parseEspAppDescriptor(makeAppHeader()), {
version: '9.5.0+stackchan.2',
moddableVersion: '9.5.0',
hostApiVersion: 2,
projectName: 'xs_esp32',
target: null,
})
assert.deepEqual(parseEspAppDescriptor(makeAppHeader('9.5.0')), {
version: '9.5.0',
moddableVersion: '9.5.0',
hostApiVersion: 0,
projectName: 'xs_esp32',
target: null,
})
assert.equal(parseEspAppDescriptor(new Uint8Array(256)), null)
})
test('findXsPartition works for a different board layout (4MB)', () => {
const table = makePartitionTable([
{ type: 0x00, subtype: 0x00, offset: 0x10000, size: 0x3a0000, label: 'factory' },
{ type: 0x40, subtype: 0x01, offset: 0x3c0000, size: 0x40000, label: 'xs' },
])
assert.equal(findXsPartition(parsePartitionTable(table)).offset, 0x3c0000)
})
test('findXsPartition throws when there is no xs partition', () => {
const table = makePartitionTable([{ type: 0x00, subtype: 0x00, offset: 0x10000, size: 0x100000, label: 'factory' }])
assert.throws(() => findXsPartition(parsePartitionTable(table)), /xs パーティションが見つかりません/)
})
test('bytesToBinaryString preserves every byte value', () => {
const bytes = Uint8Array.from({ length: 0x8000 + 257 }, (_, index) => index & 0xff)
const s = bytesToBinaryString(bytes)
assert.equal(s.length, bytes.length)
assert.deepEqual(
[...s].map((c) => c.charCodeAt(0)),
[...bytes]
)
})
test('installModToDevice returns cancellation without writing when preflight is rejected', async () => {
const calls = []
const fakeLoader = {
transport: {
async disconnect() {
calls.push('disconnect')
},
},
async main() {
return 'ESP32-S3'
},
async readFlash(address) {
if (address === PARTITION_TABLE_OFFSET) return CORES3_TABLE
if (address === 0x10000) return makeAppHeader()
throw new Error(`unexpected read: 0x${address.toString(16)}`)
},
async writeFlash() {
calls.push('write')
},
}
const result = await installModToDevice(async () => fakeLoader, {}, makeArchive(), {
onPreflight: () => false,
})
assert.equal(result.status, DEVICE_OPERATION_STATUS.CANCELLED)
assert.equal(result.operation, 'install')
assert.deepEqual(calls, ['disconnect'])
})
test('installModToDevice reads the table, targets the xs offset, and resets', async () => {
const archive = makeArchive()
const calls = []
const fakeLoader = {
async main() {
calls.push(['main'])
return 'ESP32-S3'
},
async readFlash(addr, size) {
calls.push(['readFlash', addr, size])
if (addr === PARTITION_TABLE_OFFSET) return CORES3_TABLE
if (addr === 0x10000) return makeAppHeader()
if (addr === 0xfa0000 && size === archive.length && calls.some(([name]) => name === 'writeFlash')) return archive
return new Uint8Array(size).fill(0xff)
},
async writeFlash(opts) {
calls.push(['writeFlash', opts.fileArray[0].address, opts.fileArray[0].data.length])
assert.equal(opts.fileArray[0].address, 0xfa0000)
assert.equal(opts.flashSize, 'keep')
assert.equal(opts.eraseAll, false)
assert.equal(opts.fileArray[0].data.length, archive.length)
opts.reportProgress?.(0, archive.length, archive.length)
},
async resetToRunApp() {
calls.push(['resetToRunApp'])
},
}
let progress = 0
let preflight
const result = await installModToDevice(async () => fakeLoader, {}, archive, {
onProgress: (ratio) => (progress = ratio),
onPreflight: (information) => {
preflight = information
return true
},
})
assert.deepEqual(
calls.map((c) => c[0]),
['main', 'readFlash', 'readFlash', 'writeFlash', 'readFlash', 'resetToRunApp']
)
assert.equal(progress, 1)
assert.equal(result.verified, true)
assert.equal('backup' in result, false)
assert.deepEqual(preflight.firmware, {
version: '9.5.0+stackchan.2',
moddableVersion: '9.5.0',
hostApiVersion: 2,
projectName: 'xs_esp32',
target: null,
})
})
test('installModToDevice does not reboot when readback verification fails', async () => {
const archive = makeArchive()
const calls = []
let wrote = false
const fakeLoader = {
transport: {
async disconnect() {
calls.push('disconnect')
},
},
async main() {
return 'ESP32-S3'
},
async readFlash(address, size) {
if (address === PARTITION_TABLE_OFFSET) return CORES3_TABLE
if (address === 0x10000) return makeAppHeader()
if (address === 0xfa0000 && wrote) {
const mismatched = archive.slice()
mismatched[mismatched.length - 1] ^= 0xff
return mismatched
}
throw new Error(`unexpected read: 0x${address.toString(16)} / ${size}`)
},
async writeFlash() {
wrote = true
calls.push('write')
},
async resetToRunApp() {
calls.push('reset')
},
}
await assert.rejects(
installModToDevice(async () => fakeLoader, {}, archive),
/書き込み後の検証に失敗しました/
)
assert.deepEqual(calls, ['write', 'disconnect'])
})
test('installModToDevice rejects a MOD larger than the partition', async () => {
const big = makeArchive(0x50000)
const fakeLoader = {
async main() {
return 'ESP32-S3'
},
async readFlash() {
return CORES3_TABLE
},
async writeFlash() {
throw new Error('should not write')
},
async resetToRunApp() {},
}
await assert.rejects(
installModToDevice(async () => fakeLoader, {}, big),
/MODが大きすぎます/
)
})
test('archive helpers validate the header size and compare verification bytes', () => {
const archive = makeArchive(64)
assert.equal(xsArchiveByteLength(archive), archive.length)
assert.equal(xsArchiveByteLength(new Uint8Array(8)), null)
assert.equal(equalBytes(archive, archive.slice()), true)
archive[10] = 1
assert.equal(equalBytes(archive, makeArchive(64)), false)
})
test('removeModFromDevice clears the first xs sector and reboots', async () => {
const calls = []
let wrote = false
const fakeLoader = {
async main() {
return 'ESP32-S3'
},
async readFlash(address, size) {
if (address === PARTITION_TABLE_OFFSET) return CORES3_TABLE
if (address === 0x10000) return makeAppHeader()
if (address === 0xfa0000 && wrote) return new Uint8Array(size).fill(0xff)
throw new Error(`unexpected read: 0x${address.toString(16)} / ${size}`)
},
async writeFlash(options) {
calls.push(options.fileArray[0])
wrote = true
},
async resetToRunApp() {
calls.push('reset')
},
}
let preflight
const result = await removeModFromDevice(
async () => fakeLoader,
{},
{
onPreflight: (information) => {
preflight = information
return true
},
}
)
assert.equal(calls[0].address, 0xfa0000)
assert.equal(calls[0].data.length, 4096)
assert.equal(calls[1], 'reset')
assert.equal(preflight.firmware.version, '9.5.0+stackchan.2')
assert.equal(result.verified, true)
assert.equal('backup' in result, false)
})
test('removeModFromDevice returns cancellation without writing', async () => {
const calls = []
const fakeLoader = {
transport: {
async disconnect() {
calls.push('disconnect')
},
},
async main() {
return 'ESP32-S3'
},
async readFlash(address) {
if (address === PARTITION_TABLE_OFFSET) return CORES3_TABLE
if (address === 0x10000) return makeAppHeader()
throw new Error(`unexpected read: 0x${address.toString(16)}`)
},
async writeFlash() {
calls.push('write')
},
}
const result = await removeModFromDevice(async () => fakeLoader, {}, { onPreflight: () => false })
assert.equal(result.status, DEVICE_OPERATION_STATUS.CANCELLED)
assert.equal(result.operation, 'remove')
assert.deepEqual(calls, ['disconnect'])
})
test('removeModFromDevice rejects an xs partition smaller than the blank archive', async () => {
const tinyXsTable = makePartitionTable([
{ type: 0x00, subtype: 0x00, offset: 0x10000, size: 0x100000, label: 'factory' },
{ type: 0x40, subtype: 0x01, offset: 0x110000, size: 2048, label: 'xs' },
])
const calls = []
const fakeLoader = {
async main() {
return 'ESP32-S3'
},
async readFlash(address) {
if (address === PARTITION_TABLE_OFFSET) return tinyXsTable
throw new Error(`unexpected read: 0x${address.toString(16)}`)
},
async writeFlash() {
calls.push('write')
},
}
await assert.rejects(
removeModFromDevice(async () => fakeLoader, {}),
/xsパーティションが小さすぎます/
)
assert.deepEqual(calls, [])
})
test('removeModFromDevice reports reset failure without losing verified success', async () => {
const logs = []
const prompts = []
let wrote = false
const fakeLoader = {
async main() {
return 'ESP32-S3'
},
async readFlash(address, size) {
if (address === PARTITION_TABLE_OFFSET) return CORES3_TABLE
if (address === 0x10000) return makeAppHeader()
if (address === 0xfa0000 && wrote) return new Uint8Array(size).fill(0xff)
throw new Error(`unexpected read: 0x${address.toString(16)} / ${size}`)
},
async writeFlash() {
wrote = true
},
async resetToRunApp() {
throw new Error('USB reset unavailable')
},
}
const result = await removeModFromDevice(
async () => fakeLoader,
{},
{
onLog: (message) => logs.push(message),
onPrompt: (message) => prompts.push(message),
}
)
assert.equal(result.status, DEVICE_OPERATION_STATUS.REMOVED)
assert.equal(result.verified, true)
assert.match(logs.at(-1), /自動リセットに失敗/)
assert.match(prompts.at(-1), /RESETボタン/)
})
test('WebSerial rejects another board, unknown identity and unavailable camera before user preflight or writing', async () => {
for (const [suffix, metadata, code] of [
['.rt', { ...modDefinition, targets: ['m5stackchan-cores3'] }, 'MOD_TARGET_UNSUPPORTED'],
['', { ...modDefinition, targets: ['m5stackchan-cores3'] }, 'MOD_TARGET_UNKNOWN'],
['.t2', { ...modDefinition, capabilities: ['camera'] }, 'MOD_CAPABILITY_UNAVAILABLE'],
['.sc3', { ...modDefinition, targets: ['m5stackchan-cores3'] }, null],
]) {
const archive = makeXsArchive({ metadata })
const calls = []
const loader = {
async main() {
return 'ESP32-S3'
},
async readFlash(offset) {
return offset === PARTITION_TABLE_OFFSET
? CORES3_TABLE
: offset === 0x10000
? makeAppHeader(`9.5.0+stackchan.9${suffix}`)
: archive
},
async writeFlash() {
calls.push('write')
},
async resetToRunApp() {},
transport: {
async disconnect() {
calls.push('disconnect')
},
},
}
const operation = installModToDevice(async () => loader, {}, archive, {
onPreflight() {
calls.push('preflight')
return true
},
})
if (code) {
await assert.rejects(operation, { code })
assert.deepEqual(calls, ['disconnect'])
} else {
assert.equal((await operation).status, DEVICE_OPERATION_STATUS.INSTALLED)
assert.deepEqual(calls, ['preflight', 'write', 'disconnect'])
}
}
})
test('WebSerial rejects invalid app defaults before opening a device', async () => {
for (const settings of invalidModSettings) {
const archive = makeXsArchive({ metadata: { ...modDefinition, hostApiVersion: 9, settings } })
let connections = 0
await assert.rejects(
installModToDevice(
async () => {
connections++
throw new Error('must not connect')
},
{},
archive
),
{ code: 'MOD_METADATA_INVALID' }
)
assert.equal(connections, 0)
}
})
7be0145b4This file is pinned to the selected release. Download the original to open it in your design tool.