summaryrefslogtreecommitdiff
path: root/deps/npm/node_modules/tuf-js/dist/models/file.js
blob: d6d535f6ca7872587e76fcbe3deea26c5c87771f (plain)
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TargetFile = exports.MetaFile = void 0;
const crypto_1 = __importDefault(require("crypto"));
const util_1 = __importDefault(require("util"));
const error_1 = require("../error");
const guard_1 = require("../utils/guard");
// A container with information about a particular metadata file.
//
// This class is used for Timestamp and Snapshot metadata.
class MetaFile {
    constructor(opts) {
        if (opts.version <= 0) {
            throw new error_1.ValueError('Metafile version must be at least 1');
        }
        if (opts.length !== undefined) {
            validateLength(opts.length);
        }
        this.version = opts.version;
        this.length = opts.length;
        this.hashes = opts.hashes;
        this.unrecognizedFields = opts.unrecognizedFields || {};
    }
    equals(other) {
        if (!(other instanceof MetaFile)) {
            return false;
        }
        return (this.version === other.version &&
            this.length === other.length &&
            util_1.default.isDeepStrictEqual(this.hashes, other.hashes) &&
            util_1.default.isDeepStrictEqual(this.unrecognizedFields, other.unrecognizedFields));
    }
    verify(data) {
        // Verifies that the given data matches the expected length.
        if (this.length !== undefined) {
            if (data.length !== this.length) {
                throw new error_1.LengthOrHashMismatchError(`Expected length ${this.length} but got ${data.length}`);
            }
        }
        // Verifies that the given data matches the supplied hashes.
        if (this.hashes) {
            Object.entries(this.hashes).forEach(([key, value]) => {
                let hash;
                try {
                    hash = crypto_1.default.createHash(key);
                }
                catch (e) {
                    throw new error_1.LengthOrHashMismatchError(`Hash algorithm ${key} not supported`);
                }
                const observedHash = hash.update(data).digest('hex');
                if (observedHash !== value) {
                    throw new error_1.LengthOrHashMismatchError(`Expected hash ${value} but got ${observedHash}`);
                }
            });
        }
    }
    toJSON() {
        const json = {
            version: this.version,
            ...this.unrecognizedFields,
        };
        if (this.length !== undefined) {
            json.length = this.length;
        }
        if (this.hashes) {
            json.hashes = this.hashes;
        }
        return json;
    }
    static fromJSON(data) {
        const { version, length, hashes, ...rest } = data;
        if (typeof version !== 'number') {
            throw new TypeError('version must be a number');
        }
        if ((0, guard_1.isDefined)(length) && typeof length !== 'number') {
            throw new TypeError('length must be a number');
        }
        if ((0, guard_1.isDefined)(hashes) && !(0, guard_1.isStringRecord)(hashes)) {
            throw new TypeError('hashes must be string keys and values');
        }
        return new MetaFile({
            version,
            length,
            hashes,
            unrecognizedFields: rest,
        });
    }
}
exports.MetaFile = MetaFile;
// Container for info about a particular target file.
//
// This class is used for Target metadata.
class TargetFile {
    constructor(opts) {
        validateLength(opts.length);
        this.length = opts.length;
        this.path = opts.path;
        this.hashes = opts.hashes;
        this.unrecognizedFields = opts.unrecognizedFields || {};
    }
    get custom() {
        const custom = this.unrecognizedFields['custom'];
        if (!custom || Array.isArray(custom) || !(typeof custom === 'object')) {
            return {};
        }
        return custom;
    }
    equals(other) {
        if (!(other instanceof TargetFile)) {
            return false;
        }
        return (this.length === other.length &&
            this.path === other.path &&
            util_1.default.isDeepStrictEqual(this.hashes, other.hashes) &&
            util_1.default.isDeepStrictEqual(this.unrecognizedFields, other.unrecognizedFields));
    }
    async verify(stream) {
        let observedLength = 0;
        // Create a digest for each hash algorithm
        const digests = Object.keys(this.hashes).reduce((acc, key) => {
            try {
                acc[key] = crypto_1.default.createHash(key);
            }
            catch (e) {
                throw new error_1.LengthOrHashMismatchError(`Hash algorithm ${key} not supported`);
            }
            return acc;
        }, {});
        // Read stream chunk by chunk
        for await (const chunk of stream) {
            // Keep running tally of stream length
            observedLength += chunk.length;
            // Append chunk to each digest
            Object.values(digests).forEach((digest) => {
                digest.update(chunk);
            });
        }
        // Verify length matches expected value
        if (observedLength !== this.length) {
            throw new error_1.LengthOrHashMismatchError(`Expected length ${this.length} but got ${observedLength}`);
        }
        // Verify each digest matches expected value
        Object.entries(digests).forEach(([key, value]) => {
            const expected = this.hashes[key];
            const actual = value.digest('hex');
            if (actual !== expected) {
                throw new error_1.LengthOrHashMismatchError(`Expected hash ${expected} but got ${actual}`);
            }
        });
    }
    toJSON() {
        return {
            length: this.length,
            hashes: this.hashes,
            ...this.unrecognizedFields,
        };
    }
    static fromJSON(path, data) {
        const { length, hashes, ...rest } = data;
        if (typeof length !== 'number') {
            throw new TypeError('length must be a number');
        }
        if (!(0, guard_1.isStringRecord)(hashes)) {
            throw new TypeError('hashes must have string keys and values');
        }
        return new TargetFile({
            length,
            path,
            hashes,
            unrecognizedFields: rest,
        });
    }
}
exports.TargetFile = TargetFile;
// Check that supplied length if valid
function validateLength(length) {
    if (length < 0) {
        throw new error_1.ValueError('Length must be at least 0');
    }
}