Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/lib/badge-editor-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ export type DiskDirectory = {
removeEntry: (name: string, options?: { recursive?: boolean }) => Promise<void>
}

export async function validateBadgeRoot(root: DiskDirectory) {
try {
await root.getDirectoryHandle('apps')
} catch (error) {
if (error instanceof DOMException && (error.name === 'NotFoundError' || error.name === 'TypeMismatchError')) {
throw new Error('Select the BADGER USB volume itself, rather than an app folder. The selected folder must contain the apps directory.')
}
throw error
}
}

export class ExistingAppError extends Error {
constructor() {
super('This app already exists. Open it from the badge to edit, or allow updating this app and click Upload again.')
Expand Down
5 changes: 2 additions & 3 deletions src/lib/badge-editor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ExistingAppError, uploadEditorProject, validateAppName, type DiskDirectory } from './badge-editor-upload'
import { ExistingAppError, uploadEditorProject, validateAppName, validateBadgeRoot, type DiskDirectory } from './badge-editor-upload'
import { initEditorSplit } from './badge-editor-split'
import { initProjectPicker } from './badge-editor-picker'
import { initEditorFullscreen } from './badge-editor-fullscreen'
Expand Down Expand Up @@ -494,8 +494,7 @@ export async function initBadgeEditor() {
setBusy(true)
try {
const root = await picker.call(window, { mode: 'readwrite' })
if (root.name.toUpperCase() !== 'BADGER') throw new Error('Select the BADGER USB volume itself, rather than an app folder.')
await root.getDirectoryHandle('apps')
await validateBadgeRoot(root)
const oldDisk = disk
disk = root
try { await refreshDeviceApps() } catch (error) { disk = oldDisk; throw error }
Expand Down
38 changes: 37 additions & 1 deletion tests/badge-editor-upload.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,43 @@ import { build } from 'esbuild'

const compiled = await build({ entryPoints: [fileURLToPath(new URL('../src/lib/badge-editor-upload.ts', import.meta.url))], bundle: true, format: 'esm', platform: 'node', write: false })
const moduleUrl = `data:text/javascript;base64,${Buffer.from(compiled.outputFiles[0].text).toString('base64')}`
const { uploadEditorApp, uploadEditorProject, ExistingAppError } = await import(moduleUrl)
const { uploadEditorApp, uploadEditorProject, validateBadgeRoot, ExistingAppError } = await import(moduleUrl)

test('badge root validation accepts Windows drive names and renamed volumes without writing', async () => {
for (const name of ['BADGER', 'E:', 'E:\\', 'BADGER (E:)', 'MY-BADGE']) {
const checked = []
await validateBadgeRoot({
name,
async getDirectoryHandle(path, options) {
checked.push(path)
assert.equal(options, undefined)
return {}
},
})
assert.deepEqual(checked, ['apps'])
}
})

test('badge root validation rejects app folders and volumes without an apps directory', async () => {
for (const name of ['my-app', 'apps', 'BADGER', 'RP2350', 'E:']) {
for (const errorName of ['NotFoundError', 'TypeMismatchError']) {
await assert.rejects(validateBadgeRoot({
name,
async getDirectoryHandle() { throw new DOMException('Invalid apps directory', errorName) },
}), /Select the BADGER USB volume itself/)
}
}
})

test('badge root validation preserves permission and device errors', async () => {
for (const errorName of ['NotAllowedError', 'NotReadableError']) {
const error = new DOMException('Cannot read device', errorName)
await assert.rejects(validateBadgeRoot({
name: 'E:',
async getDirectoryHandle() { throw error },
}), thrown => thrown === error)
}
})

function filesystem(existing = false, failure = '') {
const files = new Map(existing ? [['__init__.py', 'original'], ['icon.png', 'icon']] : [])
Expand Down
Loading