80 lines
2.5 KiB
TypeScript
80 lines
2.5 KiB
TypeScript
|
|
/**
|
||
|
|
* sdCardFiles.ts — assemble the microSD card's FAT16 image from the files that
|
||
|
|
* should live on it.
|
||
|
|
*
|
||
|
|
* Two sources, matching the Wokwi model:
|
||
|
|
* - FREE: the project's workspace files are auto-copied (text sources, etc.).
|
||
|
|
* - PAID: user-uploaded files (binaries included) from the "SD Card" panel.
|
||
|
|
*
|
||
|
|
* Uploaded files win over a project file of the same name. The result is a
|
||
|
|
* Uint8Array disk image handed to the `microsd-card` simulation part via
|
||
|
|
* `element.sdImageData`.
|
||
|
|
*/
|
||
|
|
import { buildFat16Image, type SdFile, type BuildFatOptions } from './fatImage';
|
||
|
|
|
||
|
|
export interface WorkspaceFileLike {
|
||
|
|
name: string;
|
||
|
|
content: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** An uploaded SD file as persisted on the component (`properties.sdFiles`). */
|
||
|
|
export interface UploadedSdFile {
|
||
|
|
name: string;
|
||
|
|
/** Base64 of the raw bytes (binaries included). */
|
||
|
|
contentB64: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Max total size of uploaded files — matches the FAT16 volume (Wokwi ~8 MB). */
|
||
|
|
export const SD_UPLOAD_MAX_BYTES = 8 * 1024 * 1024;
|
||
|
|
|
||
|
|
function b64ToBytes(b64: string): Uint8Array {
|
||
|
|
const bin = atob(b64);
|
||
|
|
const out = new Uint8Array(bin.length);
|
||
|
|
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function bytesToB64(bytes: Uint8Array): string {
|
||
|
|
let bin = '';
|
||
|
|
const CHUNK = 0x8000;
|
||
|
|
for (let i = 0; i < bytes.length; i += CHUNK) {
|
||
|
|
bin += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
||
|
|
}
|
||
|
|
return btoa(bin);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Decode the persisted `properties.sdFiles` into builder-ready SdFile[]. */
|
||
|
|
export function decodeSdFiles(raw: unknown): SdFile[] {
|
||
|
|
if (!Array.isArray(raw)) return [];
|
||
|
|
const out: SdFile[] = [];
|
||
|
|
for (const e of raw) {
|
||
|
|
if (e && typeof e.name === 'string' && typeof e.contentB64 === 'string') {
|
||
|
|
try {
|
||
|
|
out.push({ name: e.name, data: b64ToBytes(e.contentB64) });
|
||
|
|
} catch {
|
||
|
|
/* skip malformed entry */
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Build the SD image from the project workspace files (auto-copied, free) plus
|
||
|
|
* any uploaded files (paid). Uploaded files override same-named project files.
|
||
|
|
*/
|
||
|
|
export function buildProjectSdImage(
|
||
|
|
workspaceFiles: WorkspaceFileLike[],
|
||
|
|
uploaded: SdFile[] = [],
|
||
|
|
opts?: BuildFatOptions,
|
||
|
|
): Uint8Array {
|
||
|
|
const byName = new Map<string, SdFile>();
|
||
|
|
for (const f of workspaceFiles) {
|
||
|
|
byName.set(f.name.toLowerCase(), { name: f.name, data: new TextEncoder().encode(f.content) });
|
||
|
|
}
|
||
|
|
for (const f of uploaded) {
|
||
|
|
byName.set(f.name.toLowerCase(), f);
|
||
|
|
}
|
||
|
|
return buildFat16Image([...byName.values()], opts);
|
||
|
|
}
|