File size: 2,861 Bytes
c3633b1
0ed8124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c3633b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ed8124
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import { describe, expect, it, vi } from 'vitest';
import { verifyBlobSha256 } from './blob-integrity';

describe('verifyBlobSha256', () => {
  it('accepts the known SHA-256 for a streamed blob', async () => {
    const result = await verifyBlobSha256(
      new Blob(['abc']),
      'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',
    );

    expect(result).toEqual({
      actualSha256: 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',
      matches: true,
    });
  });

  it('rejects same-size content with a different digest', async () => {
    const expected = await verifyBlobSha256(new Blob(['good']), '0'.repeat(64));
    const corrupted = await verifyBlobSha256(new Blob(['evil']), expected.actualSha256);

    expect(new Blob(['good']).size).toBe(new Blob(['evil']).size);
    expect(corrupted.matches).toBe(false);
    expect(corrupted.actualSha256).not.toBe(expected.actualSha256);
  });

  it('reports monotonic byte progress while hashing a shard', async () => {
    const data = new Uint8Array(256 * 1024);
    data.fill(0x61);
    const blob = new Blob([data]);
    Object.defineProperty(blob, 'stream', {
      value: () => {
        let offset = 0;
        return new ReadableStream<Uint8Array>({
          pull(controller) {
            if (offset >= data.byteLength) {
              controller.close();
              return;
            }
            const nextOffset = Math.min(data.byteLength, offset + (64 * 1024));
            controller.enqueue(data.subarray(offset, nextOffset));
            offset = nextOffset;
          },
        });
      },
    });
    const progress: Array<{ loadedBytes: number; totalBytes: number }> = [];
    let timestamp = 0;
    const now = vi.spyOn(performance, 'now').mockImplementation(() => {
      timestamp += 101;
      return timestamp;
    });

    try {
      await verifyBlobSha256(blob, '0'.repeat(64), undefined, (loadedBytes, totalBytes) => {
        progress.push({ loadedBytes, totalBytes });
      });
    } finally {
      now.mockRestore();
    }

    expect(progress[0]).toEqual({ loadedBytes: 0, totalBytes: blob.size });
    expect(progress.at(-1)).toEqual({ loadedBytes: blob.size, totalBytes: blob.size });
    expect(progress.some(({ loadedBytes }) => loadedBytes > 0 && loadedBytes < blob.size)).toBe(true);
    expect(progress.every(({ totalBytes }) => totalBytes === blob.size)).toBe(true);
    expect(progress.every((sample, index) => (
      index === 0 || sample.loadedBytes >= (progress[index - 1]?.loadedBytes ?? 0)
    ))).toBe(true);
  });

  it('stops hashing when the caller aborts', async () => {
    const controller = new AbortController();
    controller.abort();

    await expect(
      verifyBlobSha256(new Blob(['abc']), '0'.repeat(64), controller.signal),
    ).rejects.toMatchObject({ name: 'AbortError' });
  });
});