fix(18): Manifest-Eintraege in getManifest() auf gueltige Form pruefen (WR-03)

Ein kaputter Plattform-Eintrag (z. B. fehlendes name-Feld oder ungueltiger
sha256) fiel bisher erst spaeter unbemerkt durch -- entry.name === undefined
wurde zu "undefined" gecoerct und als Dateiname gesucht. getManifest()
prueft jetzt jeden vorhandenen Plattform-Eintrag (name: string, size: number,
sha256: 64-stelliger Hex-String) und behandelt ein kaputtes Manifest wie ein
fehlendes (404 + Warn-Log), statt die kaputte Form durchzureichen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-16 17:38:50 +02:00
parent 65efdf6ab7
commit a8964f1a23
2 changed files with 66 additions and 2 deletions
+27 -1
View File
@@ -161,7 +161,33 @@ describe('DesktopService/DesktopController — HTTP-Durchstich (Phase 18)', () =
expect(res.status).toBe(404);
});
it('Test 8 (bewusst oeffentlich): getLatest und download tragen @Public()', () => {
it('Test 9 (WR-03, ungueltiger Eintrag im Manifest): name fehlt -- getLatest wirft NotFoundException statt "undefined" als Datei zu suchen', () => {
// Bewusst am `writeManifest()`-Helper vorbei direkt geschrieben -- dessen
// Parametertyp verlangt `name`, hier soll aber genau dessen Fehlen
// geprueft werden (kaputtes Manifest, kein TS-Typfehler im Test).
fs.writeFileSync(
path.join(tempDir, 'manifest.json'),
JSON.stringify({
version: '1.1.0',
channel: 'dev',
commit: 'abc1234',
buildTime: '2026-09-16T00:00:00Z',
files: { linux: { size: 123, sha256: 'a'.repeat(64) } },
}),
);
const service = new DesktopService();
expect(() => service.getLatest()).toThrow(NotFoundException);
});
it('Test 10 (WR-03, ungueltiger Eintrag im Manifest): sha256 ist kein 64-stelliger Hex-String -- 404', async () => {
writeManifest({
linux: { name: PACKAGE_NAME, size: packageSize, sha256: 'not-a-hash' },
});
const res = await fetch(`${baseUrl}/desktop/download/linux`);
expect(res.status).toBe(404);
});
it('Test 11 (bewusst oeffentlich): getLatest und download tragen @Public()', () => {
expect(Reflect.getMetadata(IS_PUBLIC_KEY, DesktopController.prototype.getLatest)).toBe(true);
expect(Reflect.getMetadata(IS_PUBLIC_KEY, DesktopController.prototype.download)).toBe(true);
});
+39 -1
View File
@@ -15,6 +15,30 @@ import * as path from 'path';
*/
const PLATFORMS = ['windows', 'linux'] as const;
/** sha256 als Hex-String -- genau 64 Zeichen, 0-9/a-f (Gross-/Kleinschreibung egal). */
const SHA256_HEX_RE = /^[a-f0-9]{64}$/i;
/**
* Grundform eines einzelnen Datei-Eintrags im Manifest (WR-03, Code-Review
* Phase 18): `getManifest()` prueft bislang nur die Kopf-Form, nicht die
* einzelnen Plattform-Eintraege -- ein kaputter/unvollstaendiger Eintrag
* wuerde sonst unbemerkt bis in `getPackage()` durchgereicht (z. B.
* `entry.name === undefined`, das sich zu `"undefined"` coerct und dort
* fehlleitend als Dateiname gesucht wird).
*/
function isValidManifestFileEntry(entry: unknown): entry is DesktopManifestFile {
if (typeof entry !== 'object' || entry === null) {
return false;
}
const candidate = entry as Record<string, unknown>;
return (
typeof candidate.name === 'string' &&
typeof candidate.size === 'number' &&
typeof candidate.sha256 === 'string' &&
SHA256_HEX_RE.test(candidate.sha256)
);
}
@Injectable()
export class DesktopService {
private readonly logger = new Logger(DesktopService.name);
@@ -45,10 +69,24 @@ export class DesktopService {
try {
const raw = fs.readFileSync(manifestPath, 'utf-8');
const parsed = JSON.parse(raw) as DesktopManifest;
if (typeof parsed.version !== 'string' || typeof parsed.files !== 'object' || parsed.files === null) {
if (
typeof parsed.version !== 'string' ||
typeof parsed.files !== 'object' ||
parsed.files === null ||
Array.isArray(parsed.files)
) {
this.logger.warn(`manifest.json unter ${manifestPath} hat unerwartete Form`);
return null;
}
for (const platform of PLATFORMS) {
const entry = parsed.files[platform];
if (entry !== undefined && !isValidManifestFileEntry(entry)) {
this.logger.warn(
`manifest.json unter ${manifestPath} hat einen ungueltigen Eintrag fuer Plattform ${platform}`,
);
return null;
}
}
return parsed;
} catch (error) {
this.logger.warn(`manifest.json unter ${manifestPath} konnte nicht gelesen werden: ${error}`);