Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -3,24 +3,51 @@ import { mkdtemp, mkdir, readFile, readdir, rm, symlink } from 'node:fs/promises
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { assertRegularPath, manifestChecksum, parseByteRange, receiveGameFile, validateManifest, validateGamePath, verifyGameFile } from '../../src/lib/server/game-files/storage.js';
|
||||
import {
|
||||
assertRegularPath,
|
||||
manifestChecksum,
|
||||
parseByteRange,
|
||||
receiveGameFile,
|
||||
validateManifest,
|
||||
validateGamePath,
|
||||
verifyGameFile
|
||||
} from '../../src/lib/server/game-files/storage.js';
|
||||
|
||||
const sha = (data: string) => createHash('sha256').update(data).digest('hex');
|
||||
const entry = (path = 'zone/file.ff', data = 'game data') => ({ path, size: Buffer.byteLength(data), sha256: sha(data) });
|
||||
const entry = (path = 'zone/file.ff', data = 'game data') => ({
|
||||
path,
|
||||
size: Buffer.byteLength(data),
|
||||
sha256: sha(data)
|
||||
});
|
||||
const stream = (value: string) => new Response(value).body!;
|
||||
let directory: string;
|
||||
afterEach(async () => { if (directory) await rm(directory, { recursive: true, force: true }); });
|
||||
afterEach(async () => {
|
||||
if (directory) await rm(directory, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('game-file manifests', () => {
|
||||
it.each(['../escape', '/absolute', 'a/../b', 'a//b', 'a\\b', 'C:/file', './file', 'a\0b', 'a/file.', 'a/file '])('rejects unsafe or ambiguous path %s', path => {
|
||||
it.each([
|
||||
'../escape',
|
||||
'/absolute',
|
||||
'a/../b',
|
||||
'a//b',
|
||||
'a\\b',
|
||||
'C:/file',
|
||||
'./file',
|
||||
'a\0b',
|
||||
'a/file.',
|
||||
'a/file '
|
||||
])('rejects unsafe or ambiguous path %s', (path) => {
|
||||
expect(() => validateGamePath(path)).toThrow();
|
||||
});
|
||||
it('canonicalizes file order, rejects duplicates, and hashes the canonical manifest', () => {
|
||||
const files = validateManifest([entry('z.ff'), entry('a.ff')]);
|
||||
expect(files.map(file => file.path)).toEqual(['a.ff', 'z.ff']);
|
||||
expect(files.map((file) => file.path)).toEqual(['a.ff', 'z.ff']);
|
||||
expect(manifestChecksum(files)).toBe(sha(JSON.stringify(files)));
|
||||
expect(() => validateManifest([entry('A.ff'), entry('a.ff')])).toThrow(/Duplicate/);
|
||||
expect(() => validateManifest([entry('zone'), entry('zone/file.ff')])).toThrow(/file and a directory/);
|
||||
expect(() => validateManifest([entry('zone'), entry('zone/file.ff')])).toThrow(
|
||||
/file and a directory/
|
||||
);
|
||||
expect(() => validateManifest([{ ...entry(), size: -1 }])).toThrow();
|
||||
expect(() => validateManifest([{ ...entry(), sha256: 'bad' }])).toThrow();
|
||||
});
|
||||
@@ -31,18 +58,26 @@ describe('streaming storage integrity', () => {
|
||||
const file = entry();
|
||||
await expect(receiveGameFile(directory, file, stream('bad'))).rejects.toThrow(/mismatch/);
|
||||
expect(await readdir(join(directory, 'zone'))).toEqual([]);
|
||||
await expect(receiveGameFile(directory, file, stream('game data too long'))).rejects.toThrow(/exceeds/);
|
||||
await expect(
|
||||
receiveGameFile(directory, file, stream('game data too long'))
|
||||
).rejects.toThrow(/exceeds/);
|
||||
await receiveGameFile(directory, file, stream('game data'));
|
||||
expect(await readFile(join(directory, file.path), 'utf8')).toBe('game data');
|
||||
await verifyGameFile(directory, file);
|
||||
await expect(verifyGameFile(directory, { ...file, sha256: sha('something') })).rejects.toThrow(/Checksum/);
|
||||
await expect(
|
||||
verifyGameFile(directory, { ...file, sha256: sha('something') })
|
||||
).rejects.toThrow(/Checksum/);
|
||||
});
|
||||
it('refuses symlinks before writing or reading shared files', async () => {
|
||||
directory = await mkdtemp(join(tmpdir(), 'ec-game-symlink-'));
|
||||
const root = join(directory, 'root'); await mkdir(root);
|
||||
const outside = join(directory, 'outside'); await mkdir(outside);
|
||||
const root = join(directory, 'root');
|
||||
await mkdir(root);
|
||||
const outside = join(directory, 'outside');
|
||||
await mkdir(outside);
|
||||
await symlink(outside, join(root, 'zone'));
|
||||
await expect(receiveGameFile(root, entry(), stream('game data'))).rejects.toThrow(/non-regular/);
|
||||
await expect(receiveGameFile(root, entry(), stream('game data'))).rejects.toThrow(
|
||||
/non-regular/
|
||||
);
|
||||
await expect(assertRegularPath(root, 'zone/file.ff', true)).rejects.toThrow(/non-regular/);
|
||||
expect(await readdir(outside)).toEqual([]);
|
||||
});
|
||||
@@ -55,7 +90,10 @@ describe('HTTP download resume ranges', () => {
|
||||
expect(parseByteRange('bytes=-3', 10)).toEqual({ start: 7, end: 9 });
|
||||
expect(parseByteRange('bytes=3-99', 10)).toEqual({ start: 3, end: 9 });
|
||||
});
|
||||
it.each(['bytes=10-', 'bytes=-0', 'bytes=5-3', 'bytes=0-1,3-4', 'bytes=-', 'not-a-range'])('rejects unsatisfiable %s', range => {
|
||||
expect(() => parseByteRange(range, 10)).toThrow();
|
||||
});
|
||||
it.each(['bytes=10-', 'bytes=-0', 'bytes=5-3', 'bytes=0-1,3-4', 'bytes=-', 'not-a-range'])(
|
||||
'rejects unsatisfiable %s',
|
||||
(range) => {
|
||||
expect(() => parseByteRange(range, 10)).toThrow();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user