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);
|
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.getLatest)).toBe(true);
|
||||||
expect(Reflect.getMetadata(IS_PUBLIC_KEY, DesktopController.prototype.download)).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;
|
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()
|
@Injectable()
|
||||||
export class DesktopService {
|
export class DesktopService {
|
||||||
private readonly logger = new Logger(DesktopService.name);
|
private readonly logger = new Logger(DesktopService.name);
|
||||||
@@ -45,10 +69,24 @@ export class DesktopService {
|
|||||||
try {
|
try {
|
||||||
const raw = fs.readFileSync(manifestPath, 'utf-8');
|
const raw = fs.readFileSync(manifestPath, 'utf-8');
|
||||||
const parsed = JSON.parse(raw) as DesktopManifest;
|
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`);
|
this.logger.warn(`manifest.json unter ${manifestPath} hat unerwartete Form`);
|
||||||
return null;
|
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;
|
return parsed;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.warn(`manifest.json unter ${manifestPath} konnte nicht gelesen werden: ${error}`);
|
this.logger.warn(`manifest.json unter ${manifestPath} konnte nicht gelesen werden: ${error}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user