• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"use strict";
2var __importDefault = (this && this.__importDefault) || function (mod) {
3    return (mod && mod.__esModule) ? mod : { "default": mod };
4};
5Object.defineProperty(exports, "__esModule", { value: true });
6exports.TargetFile = exports.MetaFile = void 0;
7const crypto_1 = __importDefault(require("crypto"));
8const util_1 = __importDefault(require("util"));
9const error_1 = require("./error");
10const utils_1 = require("./utils");
11// A container with information about a particular metadata file.
12//
13// This class is used for Timestamp and Snapshot metadata.
14class MetaFile {
15    constructor(opts) {
16        if (opts.version <= 0) {
17            throw new error_1.ValueError('Metafile version must be at least 1');
18        }
19        if (opts.length !== undefined) {
20            validateLength(opts.length);
21        }
22        this.version = opts.version;
23        this.length = opts.length;
24        this.hashes = opts.hashes;
25        this.unrecognizedFields = opts.unrecognizedFields || {};
26    }
27    equals(other) {
28        if (!(other instanceof MetaFile)) {
29            return false;
30        }
31        return (this.version === other.version &&
32            this.length === other.length &&
33            util_1.default.isDeepStrictEqual(this.hashes, other.hashes) &&
34            util_1.default.isDeepStrictEqual(this.unrecognizedFields, other.unrecognizedFields));
35    }
36    verify(data) {
37        // Verifies that the given data matches the expected length.
38        if (this.length !== undefined) {
39            if (data.length !== this.length) {
40                throw new error_1.LengthOrHashMismatchError(`Expected length ${this.length} but got ${data.length}`);
41            }
42        }
43        // Verifies that the given data matches the supplied hashes.
44        if (this.hashes) {
45            Object.entries(this.hashes).forEach(([key, value]) => {
46                let hash;
47                try {
48                    hash = crypto_1.default.createHash(key);
49                }
50                catch (e) {
51                    throw new error_1.LengthOrHashMismatchError(`Hash algorithm ${key} not supported`);
52                }
53                const observedHash = hash.update(data).digest('hex');
54                if (observedHash !== value) {
55                    throw new error_1.LengthOrHashMismatchError(`Expected hash ${value} but got ${observedHash}`);
56                }
57            });
58        }
59    }
60    toJSON() {
61        const json = {
62            version: this.version,
63            ...this.unrecognizedFields,
64        };
65        if (this.length !== undefined) {
66            json.length = this.length;
67        }
68        if (this.hashes) {
69            json.hashes = this.hashes;
70        }
71        return json;
72    }
73    static fromJSON(data) {
74        const { version, length, hashes, ...rest } = data;
75        if (typeof version !== 'number') {
76            throw new TypeError('version must be a number');
77        }
78        if (utils_1.guard.isDefined(length) && typeof length !== 'number') {
79            throw new TypeError('length must be a number');
80        }
81        if (utils_1.guard.isDefined(hashes) && !utils_1.guard.isStringRecord(hashes)) {
82            throw new TypeError('hashes must be string keys and values');
83        }
84        return new MetaFile({
85            version,
86            length,
87            hashes,
88            unrecognizedFields: rest,
89        });
90    }
91}
92exports.MetaFile = MetaFile;
93// Container for info about a particular target file.
94//
95// This class is used for Target metadata.
96class TargetFile {
97    constructor(opts) {
98        validateLength(opts.length);
99        this.length = opts.length;
100        this.path = opts.path;
101        this.hashes = opts.hashes;
102        this.unrecognizedFields = opts.unrecognizedFields || {};
103    }
104    get custom() {
105        const custom = this.unrecognizedFields['custom'];
106        if (!custom || Array.isArray(custom) || !(typeof custom === 'object')) {
107            return {};
108        }
109        return custom;
110    }
111    equals(other) {
112        if (!(other instanceof TargetFile)) {
113            return false;
114        }
115        return (this.length === other.length &&
116            this.path === other.path &&
117            util_1.default.isDeepStrictEqual(this.hashes, other.hashes) &&
118            util_1.default.isDeepStrictEqual(this.unrecognizedFields, other.unrecognizedFields));
119    }
120    async verify(stream) {
121        let observedLength = 0;
122        // Create a digest for each hash algorithm
123        const digests = Object.keys(this.hashes).reduce((acc, key) => {
124            try {
125                acc[key] = crypto_1.default.createHash(key);
126            }
127            catch (e) {
128                throw new error_1.LengthOrHashMismatchError(`Hash algorithm ${key} not supported`);
129            }
130            return acc;
131        }, {});
132        // Read stream chunk by chunk
133        for await (const chunk of stream) {
134            // Keep running tally of stream length
135            observedLength += chunk.length;
136            // Append chunk to each digest
137            Object.values(digests).forEach((digest) => {
138                digest.update(chunk);
139            });
140        }
141        // Verify length matches expected value
142        if (observedLength !== this.length) {
143            throw new error_1.LengthOrHashMismatchError(`Expected length ${this.length} but got ${observedLength}`);
144        }
145        // Verify each digest matches expected value
146        Object.entries(digests).forEach(([key, value]) => {
147            const expected = this.hashes[key];
148            const actual = value.digest('hex');
149            if (actual !== expected) {
150                throw new error_1.LengthOrHashMismatchError(`Expected hash ${expected} but got ${actual}`);
151            }
152        });
153    }
154    toJSON() {
155        return {
156            length: this.length,
157            hashes: this.hashes,
158            ...this.unrecognizedFields,
159        };
160    }
161    static fromJSON(path, data) {
162        const { length, hashes, ...rest } = data;
163        if (typeof length !== 'number') {
164            throw new TypeError('length must be a number');
165        }
166        if (!utils_1.guard.isStringRecord(hashes)) {
167            throw new TypeError('hashes must have string keys and values');
168        }
169        return new TargetFile({
170            length,
171            path,
172            hashes,
173            unrecognizedFields: rest,
174        });
175    }
176}
177exports.TargetFile = TargetFile;
178// Check that supplied length if valid
179function validateLength(length) {
180    if (length < 0) {
181        throw new error_1.ValueError('Length must be at least 0');
182    }
183}
184