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