1"use strict";
2var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3    if (k2 === undefined) k2 = k;
4    var desc = Object.getOwnPropertyDescriptor(m, k);
5    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6      desc = { enumerable: true, get: function() { return m[k]; } };
7    }
8    Object.defineProperty(o, k2, desc);
9}) : (function(o, m, k, k2) {
10    if (k2 === undefined) k2 = k;
11    o[k2] = m[k];
12}));
13var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14    Object.defineProperty(o, "default", { enumerable: true, value: v });
15}) : function(o, v) {
16    o["default"] = v;
17});
18var __importStar = (this && this.__importStar) || function (mod) {
19    if (mod && mod.__esModule) return mod;
20    var result = {};
21    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22    __setModuleDefault(result, mod);
23    return result;
24};
25var __importDefault = (this && this.__importDefault) || function (mod) {
26    return (mod && mod.__esModule) ? mod : { "default": mod };
27};
28Object.defineProperty(exports, "__esModule", { value: true });
29exports.Updater = void 0;
30const models_1 = require("@tufjs/models");
31const debug_1 = __importDefault(require("debug"));
32const fs = __importStar(require("fs"));
33const path = __importStar(require("path"));
34const config_1 = require("./config");
35const error_1 = require("./error");
36const fetcher_1 = require("./fetcher");
37const store_1 = require("./store");
38const url = __importStar(require("./utils/url"));
39const log = (0, debug_1.default)('tuf:cache');
40class Updater {
41    constructor(options) {
42        const { metadataDir, metadataBaseUrl, targetDir, targetBaseUrl, fetcher, config, } = options;
43        this.dir = metadataDir;
44        this.metadataBaseUrl = metadataBaseUrl;
45        this.targetDir = targetDir;
46        this.targetBaseUrl = targetBaseUrl;
47        this.forceCache = options.forceCache ?? false;
48        const data = this.loadLocalMetadata(models_1.MetadataKind.Root);
49        this.trustedSet = new store_1.TrustedMetadataStore(data);
50        this.config = { ...config_1.defaultConfig, ...config };
51        this.fetcher =
52            fetcher ||
53                new fetcher_1.DefaultFetcher({
54                    timeout: this.config.fetchTimeout,
55                    retry: this.config.fetchRetries ?? this.config.fetchRetry,
56                });
57    }
58    // refresh and load the metadata before downloading the target
59    // refresh should be called once after the client is initialized
60    async refresh() {
61        // If forceCache is true, try to load the timestamp from local storage
62        // without fetching it from the remote. Otherwise, load the root and
63        // timestamp from the remote per the TUF spec.
64        if (this.forceCache) {
65            // If anything fails, load the root and timestamp from the remote. This
66            // should cover any situation where the local metadata is corrupted or
67            // expired.
68            try {
69                await this.loadTimestamp({ checkRemote: false });
70            }
71            catch (error) {
72                await this.loadRoot();
73                await this.loadTimestamp();
74            }
75        }
76        else {
77            await this.loadRoot();
78            await this.loadTimestamp();
79        }
80        await this.loadSnapshot();
81        await this.loadTargets(models_1.MetadataKind.Targets, models_1.MetadataKind.Root);
82    }
83    // Returns the TargetFile instance with information for the given target path.
84    //
85    // Implicitly calls refresh if it hasn't already been called.
86    async getTargetInfo(targetPath) {
87        if (!this.trustedSet.targets) {
88            await this.refresh();
89        }
90        return this.preorderDepthFirstWalk(targetPath);
91    }
92    async downloadTarget(targetInfo, filePath, targetBaseUrl) {
93        const targetPath = filePath || this.generateTargetPath(targetInfo);
94        if (!targetBaseUrl) {
95            if (!this.targetBaseUrl) {
96                throw new error_1.ValueError('Target base URL not set');
97            }
98            targetBaseUrl = this.targetBaseUrl;
99        }
100        let targetFilePath = targetInfo.path;
101        const consistentSnapshot = this.trustedSet.root.signed.consistentSnapshot;
102        if (consistentSnapshot && this.config.prefixTargetsWithHash) {
103            const hashes = Object.values(targetInfo.hashes);
104            const { dir, base } = path.parse(targetFilePath);
105            const filename = `${hashes[0]}.${base}`;
106            targetFilePath = dir ? `${dir}/${filename}` : filename;
107        }
108        const targetUrl = url.join(targetBaseUrl, targetFilePath);
109        // Client workflow 5.7.3: download target file
110        await this.fetcher.downloadFile(targetUrl, targetInfo.length, async (fileName) => {
111            // Verify hashes and length of downloaded file
112            await targetInfo.verify(fs.createReadStream(fileName));
113            // Copy file to target path
114            log('WRITE %s', targetPath);
115            fs.copyFileSync(fileName, targetPath);
116        });
117        return targetPath;
118    }
119    async findCachedTarget(targetInfo, filePath) {
120        if (!filePath) {
121            filePath = this.generateTargetPath(targetInfo);
122        }
123        try {
124            if (fs.existsSync(filePath)) {
125                await targetInfo.verify(fs.createReadStream(filePath));
126                return filePath;
127            }
128        }
129        catch (error) {
130            return; // File not found
131        }
132        return; // File not found
133    }
134    loadLocalMetadata(fileName) {
135        const filePath = path.join(this.dir, `${fileName}.json`);
136        log('READ %s', filePath);
137        return fs.readFileSync(filePath);
138    }
139    // Sequentially load and persist on local disk every newer root metadata
140    // version available on the remote.
141    // Client workflow 5.3: update root role
142    async loadRoot() {
143        // Client workflow 5.3.2: version of trusted root metadata file
144        const rootVersion = this.trustedSet.root.signed.version;
145        const lowerBound = rootVersion + 1;
146        const upperBound = lowerBound + this.config.maxRootRotations;
147        for (let version = lowerBound; version <= upperBound; version++) {
148            const rootUrl = url.join(this.metadataBaseUrl, `${version}.root.json`);
149            try {
150                // Client workflow 5.3.3: download new root metadata file
151                const bytesData = await this.fetcher.downloadBytes(rootUrl, this.config.rootMaxLength);
152                // Client workflow 5.3.4 - 5.4.7
153                this.trustedSet.updateRoot(bytesData);
154                // Client workflow 5.3.8: persist root metadata file
155                this.persistMetadata(models_1.MetadataKind.Root, bytesData);
156            }
157            catch (error) {
158                break;
159            }
160        }
161    }
162    // Load local and remote timestamp metadata.
163    // Client workflow 5.4: update timestamp role
164    async loadTimestamp({ checkRemote } = { checkRemote: true }) {
165        // Load local and remote timestamp metadata
166        try {
167            const data = this.loadLocalMetadata(models_1.MetadataKind.Timestamp);
168            this.trustedSet.updateTimestamp(data);
169            // If checkRemote is disabled, return here to avoid fetching the remote
170            // timestamp metadata.
171            if (!checkRemote) {
172                return;
173            }
174        }
175        catch (error) {
176            // continue
177        }
178        //Load from remote (whether local load succeeded or not)
179        const timestampUrl = url.join(this.metadataBaseUrl, 'timestamp.json');
180        // Client workflow 5.4.1: download timestamp metadata file
181        const bytesData = await this.fetcher.downloadBytes(timestampUrl, this.config.timestampMaxLength);
182        try {
183            // Client workflow 5.4.2 - 5.4.4
184            this.trustedSet.updateTimestamp(bytesData);
185        }
186        catch (error) {
187            // If new timestamp version is same as current, discardd the new one.
188            // This is normal and should NOT raise an error.
189            if (error instanceof error_1.EqualVersionError) {
190                return;
191            }
192            // Re-raise any other error
193            throw error;
194        }
195        // Client workflow 5.4.5: persist timestamp metadata
196        this.persistMetadata(models_1.MetadataKind.Timestamp, bytesData);
197    }
198    // Load local and remote snapshot metadata.
199    // Client workflow 5.5: update snapshot role
200    async loadSnapshot() {
201        //Load local (and if needed remote) snapshot metadata
202        try {
203            const data = this.loadLocalMetadata(models_1.MetadataKind.Snapshot);
204            this.trustedSet.updateSnapshot(data, true);
205        }
206        catch (error) {
207            if (!this.trustedSet.timestamp) {
208                throw new ReferenceError('No timestamp metadata');
209            }
210            const snapshotMeta = this.trustedSet.timestamp.signed.snapshotMeta;
211            const maxLength = snapshotMeta.length || this.config.snapshotMaxLength;
212            const version = this.trustedSet.root.signed.consistentSnapshot
213                ? snapshotMeta.version
214                : undefined;
215            const snapshotUrl = url.join(this.metadataBaseUrl, version ? `${version}.snapshot.json` : 'snapshot.json');
216            try {
217                // Client workflow 5.5.1: download snapshot metadata file
218                const bytesData = await this.fetcher.downloadBytes(snapshotUrl, maxLength);
219                // Client workflow 5.5.2 - 5.5.6
220                this.trustedSet.updateSnapshot(bytesData);
221                // Client workflow 5.5.7: persist snapshot metadata file
222                this.persistMetadata(models_1.MetadataKind.Snapshot, bytesData);
223            }
224            catch (error) {
225                throw new error_1.RuntimeError(`Unable to load snapshot metadata error ${error}`);
226            }
227        }
228    }
229    // Load local and remote targets metadata.
230    // Client workflow 5.6: update targets role
231    async loadTargets(role, parentRole) {
232        if (this.trustedSet.getRole(role)) {
233            return this.trustedSet.getRole(role);
234        }
235        try {
236            const buffer = this.loadLocalMetadata(role);
237            this.trustedSet.updateDelegatedTargets(buffer, role, parentRole);
238        }
239        catch (error) {
240            // Local 'role' does not exist or is invalid: update from remote
241            if (!this.trustedSet.snapshot) {
242                throw new ReferenceError('No snapshot metadata');
243            }
244            const metaInfo = this.trustedSet.snapshot.signed.meta[`${role}.json`];
245            // TODO: use length for fetching
246            const maxLength = metaInfo.length || this.config.targetsMaxLength;
247            const version = this.trustedSet.root.signed.consistentSnapshot
248                ? metaInfo.version
249                : undefined;
250            const metadataUrl = url.join(this.metadataBaseUrl, version ? `${version}.${role}.json` : `${role}.json`);
251            try {
252                // Client workflow 5.6.1: download targets metadata file
253                const bytesData = await this.fetcher.downloadBytes(metadataUrl, maxLength);
254                // Client workflow 5.6.2 - 5.6.6
255                this.trustedSet.updateDelegatedTargets(bytesData, role, parentRole);
256                // Client workflow 5.6.7: persist targets metadata file
257                this.persistMetadata(role, bytesData);
258            }
259            catch (error) {
260                throw new error_1.RuntimeError(`Unable to load targets error ${error}`);
261            }
262        }
263        return this.trustedSet.getRole(role);
264    }
265    async preorderDepthFirstWalk(targetPath) {
266        // Interrogates the tree of target delegations in order of appearance
267        // (which implicitly order trustworthiness), and returns the matching
268        // target found in the most trusted role.
269        // List of delegations to be interrogated. A (role, parent role) pair
270        // is needed to load and verify the delegated targets metadata.
271        const delegationsToVisit = [
272            {
273                roleName: models_1.MetadataKind.Targets,
274                parentRoleName: models_1.MetadataKind.Root,
275            },
276        ];
277        const visitedRoleNames = new Set();
278        // Client workflow 5.6.7: preorder depth-first traversal of the graph of
279        // target delegations
280        while (visitedRoleNames.size <= this.config.maxDelegations &&
281            delegationsToVisit.length > 0) {
282            //  Pop the role name from the top of the stack.
283            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
284            const { roleName, parentRoleName } = delegationsToVisit.pop();
285            // Skip any visited current role to prevent cycles.
286            // Client workflow 5.6.7.1: skip already-visited roles
287            if (visitedRoleNames.has(roleName)) {
288                continue;
289            }
290            // The metadata for 'role_name' must be downloaded/updated before
291            // its targets, delegations, and child roles can be inspected.
292            const targets = (await this.loadTargets(roleName, parentRoleName))
293                ?.signed;
294            if (!targets) {
295                continue;
296            }
297            const target = targets.targets?.[targetPath];
298            if (target) {
299                return target;
300            }
301            // After preorder check, add current role to set of visited roles.
302            visitedRoleNames.add(roleName);
303            if (targets.delegations) {
304                const childRolesToVisit = [];
305                // NOTE: This may be a slow operation if there are many delegated roles.
306                const rolesForTarget = targets.delegations.rolesForTarget(targetPath);
307                for (const { role: childName, terminating } of rolesForTarget) {
308                    childRolesToVisit.push({
309                        roleName: childName,
310                        parentRoleName: roleName,
311                    });
312                    // Client workflow 5.6.7.2.1
313                    if (terminating) {
314                        delegationsToVisit.splice(0); // empty the array
315                        break;
316                    }
317                }
318                childRolesToVisit.reverse();
319                delegationsToVisit.push(...childRolesToVisit);
320            }
321        }
322        return; // no matching target found
323    }
324    generateTargetPath(targetInfo) {
325        if (!this.targetDir) {
326            throw new error_1.ValueError('Target directory not set');
327        }
328        // URL encode target path
329        const filePath = encodeURIComponent(targetInfo.path);
330        return path.join(this.targetDir, filePath);
331    }
332    persistMetadata(metaDataName, bytesData) {
333        try {
334            const filePath = path.join(this.dir, `${metaDataName}.json`);
335            log('WRITE %s', filePath);
336            fs.writeFileSync(filePath, bytesData.toString('utf8'));
337        }
338        catch (error) {
339            throw new error_1.PersistError(`Failed to persist metadata ${metaDataName} error: ${error}`);
340        }
341    }
342}
343exports.Updater = Updater;
344