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:
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
Reference in New Issue
Block a user