feat(sd): source files stay off the card - only project DATA files auto-copy

On real hardware your code lives in flash, not on the microSD; a sketch
listing the card should see its data (csv, json, txt, wav...), not its
own source. buildProjectSdImage now skips .ino/.h/.hpp/.c/.cpp/.py from
the workspace auto-copy. Explicit uploads are exempt - an upload always
lands whatever its extension. Panel hint updated; tests cover both the
exclusion and the upload exemption (15 green).
This commit is contained in:
David Montero Crespo 2026-08-04 19:58:31 +02:00
parent 2dcb3eb65f
commit 36c414df8f
3 changed files with 36 additions and 7 deletions

View File

@ -141,16 +141,34 @@ describe('buildFat16Image', () => {
});
describe('buildProjectSdImage', () => {
it('auto-copies workspace (text) files into the image', () => {
it('auto-copies workspace DATA files; source files stay off the card', () => {
const ws = [
{ name: 'sketch.ino', content: 'void setup(){}' },
{ name: 'helper.h', content: '#define X 1' },
{ name: 'impl.cpp', content: 'int x;' },
{ name: 'script.py', content: 'print(1)' },
{ name: 'notes.txt', content: 'hello from the card' },
{ name: 'config.json', content: '{}' },
];
const got = readFat16(buildProjectSdImage(ws));
const names = got.map((g) => g.name.toLowerCase());
const notes = got.find((g) => g.name.toLowerCase() === 'notes.txt');
expect(notes).toBeTruthy();
expect(new TextDecoder().decode(notes!.data)).toBe('hello from the card');
expect(got.some((g) => g.name.toLowerCase() === 'sketch.ino')).toBe(true);
expect(names).toContain('config.json');
// Code lives in flash, not on the microSD.
expect(names).not.toContain('sketch.ino');
expect(names).not.toContain('helper.h');
expect(names).not.toContain('impl.cpp');
expect(names).not.toContain('script.py');
});
it('an explicit upload lands even with a source extension', () => {
const uploaded = decodeSdFiles([
{ name: 'ref.py', contentB64: bytesToB64(new TextEncoder().encode('print(2)')) },
]);
const got = readFat16(buildProjectSdImage([], uploaded));
expect(got.some((g) => g.name.toLowerCase() === 'ref.py')).toBe(true);
});
it('uploaded (paid) files override same-named project files', () => {
@ -161,11 +179,14 @@ describe('buildProjectSdImage', () => {
expect(Array.from(f.data)).toEqual([0xde, 0xad, 0xbe, 0xef]); // binary, not "TEXT"
});
it('adds uploaded binary files alongside project files', () => {
const ws = [{ name: 'main.ino', content: 'x' }];
it('adds uploaded binary files alongside project data files', () => {
const ws = [
{ name: 'main.ino', content: 'x' }, // source: stays off the card
{ name: 'readme.txt', content: 'data' },
];
const uploaded = [{ name: 'logo.bmp', data: new Uint8Array(800).fill(0x42) }];
const got = readFat16(buildProjectSdImage(ws, uploaded));
expect(got.length).toBe(2);
expect(got.length).toBe(2); // readme.txt + logo.bmp, no main.ino
const logo = got.find((g) => g.name.toLowerCase() === 'logo.bmp')!;
expect(logo.data.length).toBe(800);
expect(logo.data.every((b) => b === 0x42)).toBe(true);

View File

@ -118,8 +118,9 @@ export const SdCardPanel: React.FC<SdCardPanelProps> = ({ files, onChange, board
</div>
{files.length === 0 && (
<div className="sd-card-hint">
Upload your own files (images, audio, data). Project files are added
automatically.
Upload your own files (images, audio, data). The project's data files
are added automatically; source files (.ino, .h, .cpp, .py) stay off
the card.
</div>
)}
{files.map((f) => (

View File

@ -59,6 +59,12 @@ export function decodeSdFiles(raw: unknown): SdFile[] {
return out;
}
/** Source files never land on the card: on real hardware your code lives in
* flash, not on the microSD, and a sketch listing the card should see its
* DATA (csv, json, txt, wav...), not its own source. Uploads are exempt —
* an explicit upload always lands, whatever its extension. */
const SD_EXCLUDED_SOURCES = /\.(ino|h|hpp|c|cpp|py)$/i;
/**
* Build the SD image from the project workspace files (auto-copied, free) plus
* any uploaded files (paid). Uploaded files override same-named project files.
@ -70,6 +76,7 @@ export function buildProjectSdImage(
): Uint8Array {
const byName = new Map<string, SdFile>();
for (const f of workspaceFiles) {
if (SD_EXCLUDED_SOURCES.test(f.name)) continue;
byName.set(f.name.toLowerCase(), { name: f.name, data: new TextEncoder().encode(f.content) });
}
for (const f of uploaded) {