UNPKG

@ethersphere/bee-js

Version:
410 lines (409 loc) 17 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MantarayNode = exports.Fork = void 0; const cafe_utility_1 = require("cafe-utility"); const debug_1 = __importDefault(require("debug")); const __1 = require(".."); const typed_bytes_1 = require("../utils/typed-bytes"); const debug = (0, debug_1.default)('bee-js:manifest'); const ENCODER = new TextEncoder(); const DECODER = new TextDecoder(); const TYPE_VALUE = 2; const TYPE_EDGE = 4; const TYPE_WITH_PATH_SEPARATOR = 8; const TYPE_WITH_METADATA = 16; const PATH_SEPARATOR = new Uint8Array([47]); const VERSION_02_HASH_HEX = '5768b3b6a7db56d21d1abff40d41cebfc83448fed8d7e9b06ec0d3b073f28f7b'; const VERSION_02_HASH = cafe_utility_1.Binary.hexToUint8Array(VERSION_02_HASH_HEX); class Fork { constructor(prefix, node) { this.prefix = prefix; this.node = node; } static split(a, b) { const commonPart = cafe_utility_1.Binary.commonPrefix(a.prefix, b.prefix); if (commonPart.length === a.prefix.length) { const remainingB = b.prefix.slice(commonPart.length); b.node.path = b.prefix.slice(commonPart.length); b.prefix = b.prefix.slice(commonPart.length); b.node.parent = a.node; a.node.forks.set(remainingB[0], b); return a; } if (commonPart.length === b.prefix.length) { const remainingA = a.prefix.slice(commonPart.length); a.node.path = a.prefix.slice(commonPart.length); a.prefix = a.prefix.slice(commonPart.length); a.node.parent = b.node; b.node.forks.set(remainingA[0], a); return b; } const node = new MantarayNode({ path: commonPart }); const newAFork = new Fork(a.prefix.slice(commonPart.length), a.node); const newBFork = new Fork(b.prefix.slice(commonPart.length), b.node); a.node.path = a.prefix.slice(commonPart.length); b.node.path = b.prefix.slice(commonPart.length); a.prefix = a.prefix.slice(commonPart.length); b.prefix = b.prefix.slice(commonPart.length); node.forks.set(newAFork.prefix[0], newAFork); node.forks.set(newBFork.prefix[0], newBFork); newAFork.node.parent = node; newBFork.node.parent = node; return new Fork(commonPart, node); } marshal() { if (!this.node.selfAddress) { throw Error('Fork#marshal node.selfAddress is not set'); } const data = []; data.push(new Uint8Array([this.node.determineType()])); data.push(cafe_utility_1.Binary.numberToUint8(this.prefix.length)); data.push(this.prefix); if (this.prefix.length < 30) { data.push(new Uint8Array(30 - this.prefix.length)); } data.push(this.node.selfAddress); debug('marshalling fork', { prefixLength: this.prefix.length, prefix: DECODER.decode(this.prefix), address: cafe_utility_1.Binary.uint8ArrayToHex(this.node.selfAddress), }); if (this.node.metadata) { const metadataBytes = cafe_utility_1.Binary.padEndToMultiple(new Uint8Array([0x00, 0x00, ...ENCODER.encode(JSON.stringify(this.node.metadata))]), 32, 0x0a); const metadataLengthBytes = cafe_utility_1.Binary.numberToUint16(metadataBytes.length - 2, 'BE'); metadataBytes.set(metadataLengthBytes, 0); data.push(metadataBytes); } return cafe_utility_1.Binary.concatBytes(...data); } static unmarshal(reader, addressLength) { const type = cafe_utility_1.Binary.uint8ToNumber(reader.read(1)); const prefixLength = cafe_utility_1.Binary.uint8ToNumber(reader.read(1)); const prefix = reader.read(prefixLength); if (prefixLength < 30) { reader.read(30 - prefixLength); } const selfAddress = reader.read(addressLength); debug('unmarshalling fork', { type, prefixLength, prefix: DECODER.decode(prefix), addressLength, address: cafe_utility_1.Binary.uint8ArrayToHex(selfAddress), }); let metadata = undefined; if (isType(type, TYPE_WITH_METADATA)) { const metadataLength = cafe_utility_1.Binary.uint16ToNumber(reader.read(2), 'BE'); metadata = JSON.parse(DECODER.decode(reader.read(metadataLength))); } return new Fork(prefix, new MantarayNode({ selfAddress, metadata, path: prefix })); } } exports.Fork = Fork; class MantarayNode { constructor(options) { this.obfuscationKey = new Uint8Array(32); this.selfAddress = null; this.targetAddress = new Uint8Array(32); this.metadata = null; this.path = new Uint8Array(0); this.forks = new Map(); this.parent = null; if (options?.targetAddress) { this.targetAddress = options.targetAddress; } if (options?.selfAddress) { this.selfAddress = options.selfAddress; } if (options?.metadata) { this.metadata = options.metadata; } if (options?.obfuscationKey) { this.obfuscationKey = options.obfuscationKey; } if (options?.path) { this.path = options.path; } if (options?.parent) { this.parent = options.parent; } } get fullPath() { return cafe_utility_1.Binary.concatBytes(this.parent?.fullPath ?? new Uint8Array(0), this.path); } get fullPathString() { return DECODER.decode(this.fullPath); } /** * Returns the metadata at the `/` path to access idiomatic properties. */ getRootMetadata() { const node = this.find('/'); if (node && node.metadata) { return cafe_utility_1.Optional.of(node.metadata); } return cafe_utility_1.Optional.empty(); } /** * Returns the `swarm-index-document` and `swarm-error-document` metadata values. */ getDocsMetadata() { const node = this.find('/'); if (!node || !node.metadata) { return { indexDocument: null, errorDocument: null }; } return { indexDocument: node.metadata['website-index-document'] ?? null, errorDocument: node.metadata['website-error-document'] ?? null, }; } /** * Attempts to resolve the manifest as a feed, returning the latest update. */ async resolveFeed(bee, requestOptions) { const node = this.find('/'); if (!node || !node.metadata) { return cafe_utility_1.Optional.empty(); } const owner = node.metadata['swarm-feed-owner']; const topic = node.metadata['swarm-feed-topic']; if (!owner || !topic) { return cafe_utility_1.Optional.empty(); } return cafe_utility_1.Optional.of(await bee.fetchLatestFeedUpdate(topic, owner, requestOptions)); } /** * Gets the binary representation of the node. */ async marshal() { for (const fork of this.forks.values()) { if (!fork.node.selfAddress) { fork.node.selfAddress = (await fork.node.calculateSelfAddress()).toUint8Array(); } } const header = new Uint8Array(32); header.set(VERSION_02_HASH, 0); header.set(cafe_utility_1.Binary.equals(this.targetAddress, __1.NULL_ADDRESS) && cafe_utility_1.Binary.equals(this.path, new Uint8Array([47])) ? cafe_utility_1.Binary.numberToUint8(0) : cafe_utility_1.Binary.numberToUint8(this.targetAddress.length), 31); const forkBitmap = new Uint8Array(32); for (const fork of this.forks.keys()) { cafe_utility_1.Binary.setBit(forkBitmap, fork, 1, 'LE'); } const forks = []; for (let i = 0; i < 256; i++) { if (cafe_utility_1.Binary.getBit(forkBitmap, i, 'LE')) { forks.push(this.forks.get(i).marshal()); } } const data = cafe_utility_1.Binary.xorCypher(cafe_utility_1.Binary.concatBytes(header, cafe_utility_1.Binary.equals(this.targetAddress, __1.NULL_ADDRESS) && cafe_utility_1.Binary.equals(this.path, new Uint8Array([47])) ? new Uint8Array(0) : this.targetAddress, forkBitmap, ...forks), this.obfuscationKey); return cafe_utility_1.Binary.concatBytes(this.obfuscationKey, data); } /** * Downloads and unmarshals a MantarayNode from the given reference. * * Do not forget calling `loadRecursively` on the returned node to load the entire tree. */ static async unmarshal(bee, reference, options, requestOptions) { reference = new typed_bytes_1.Reference(reference); const data = (await bee.downloadData(reference, options, requestOptions)).toUint8Array(); return this.unmarshalFromData(data, reference.toUint8Array()); } /** * Unmarshals a MantarayNode from the given data. * * Do not forget calling `loadRecursively` on the returned node to load the entire tree. */ static unmarshalFromData(data, selfAddress) { const obfuscationKey = data.subarray(0, 32); const decrypted = cafe_utility_1.Binary.xorCypher(data.subarray(32), obfuscationKey); const reader = new cafe_utility_1.Uint8ArrayReader(decrypted); const versionHash = reader.read(31); if (!cafe_utility_1.Binary.equals(versionHash, VERSION_02_HASH.slice(0, 31))) { throw new Error('MantarayNode#unmarshal invalid version hash'); } const targetAddressLength = cafe_utility_1.Binary.uint8ToNumber(reader.read(1)); const targetAddress = targetAddressLength ? reader.read(targetAddressLength) : __1.NULL_ADDRESS; const node = new MantarayNode({ selfAddress, targetAddress, obfuscationKey }); const forkBitmap = reader.read(32); for (let i = 0; i < 256; i++) { if (cafe_utility_1.Binary.getBit(forkBitmap, i, 'LE')) { const newFork = Fork.unmarshal(reader, selfAddress.length); node.forks.set(i, newFork); newFork.node.parent = node; } } return node; } /** * Adds a fork to the node. */ addFork(path, reference, metadata) { this.selfAddress = null; path = path instanceof Uint8Array ? path : ENCODER.encode(path); debug('adding fork', { path: DECODER.decode(path), reference: new typed_bytes_1.Reference(reference).represent() }); // TODO: this should not be ignored // eslint-disable-next-line @typescript-eslint/no-this-alias let tip = this; while (path.length) { const prefix = path.slice(0, 30); path = path.slice(30); const isLast = path.length === 0; const [bestMatch, matchedPath] = tip.findClosest(prefix); const remainingPath = prefix.slice(matchedPath.length); if (matchedPath.length) { tip = bestMatch; } if (!remainingPath.length) { continue; } const newFork = new Fork(remainingPath, new MantarayNode({ targetAddress: isLast ? new typed_bytes_1.Reference(reference).toUint8Array() : undefined, metadata: isLast ? metadata : undefined, path: remainingPath, })); const existing = bestMatch.forks.get(remainingPath[0]); if (existing) { const fork = Fork.split(newFork, existing); tip.forks.set(remainingPath[0], fork); fork.node.parent = tip; tip.selfAddress = null; tip = newFork.node; } else { tip.forks.set(remainingPath[0], newFork); newFork.node.parent = tip; tip.selfAddress = null; tip = newFork.node; } } } /** * Removes a fork from the node. */ removeFork(path) { this.selfAddress = null; path = path instanceof Uint8Array ? path : ENCODER.encode(path); if (path.length === 0) { throw Error('MantarayNode#removeFork [path] parameter cannot be empty'); } const match = this.find(path); if (!match) { throw Error('MantarayNode#removeFork fork not found'); } const [parent, matchedPath] = this.findClosest(path.slice(0, path.length - 1)); parent.forks.delete(path.slice(matchedPath.length)[0]); for (const fork of match.forks.values()) { parent.addFork(cafe_utility_1.Binary.concatBytes(match.path, fork.prefix), fork.node.targetAddress, fork.node.metadata); } } /** * Calculates the self address of the node. */ async calculateSelfAddress() { if (this.selfAddress) { return new typed_bytes_1.Reference(this.selfAddress); } return new typed_bytes_1.Reference((await cafe_utility_1.MerkleTree.root(await this.marshal())).hash()); } /** * Saves the node and its children recursively. */ async saveRecursively(bee, postageBatchId, options, requestOptions) { for (const fork of this.forks.values()) { await fork.node.saveRecursively(bee, postageBatchId, options, requestOptions); } const result = await bee.uploadData(postageBatchId, await this.marshal(), options, requestOptions); this.selfAddress = result.reference.toUint8Array(); return result; } /** * Loads the node and its children recursively. */ async loadRecursively(bee, options, requestOptions) { for (const fork of this.forks.values()) { if (!fork.node.selfAddress) { throw Error('MantarayNode#loadRecursively fork.node.selfAddress is not set'); } const node = await MantarayNode.unmarshal(bee, fork.node.selfAddress, options, requestOptions); fork.node.targetAddress = node.targetAddress; fork.node.forks = node.forks; fork.node.path = fork.prefix; fork.node.parent = this; await fork.node.loadRecursively(bee, options, requestOptions); } } /** * Finds a node in the tree by its path. */ find(path) { const [closest, match] = this.findClosest(path); return match.length === path.length ? closest : null; } /** * Finds the closest node in the tree to the given path. */ findClosest(path, current = new Uint8Array()) { path = path instanceof Uint8Array ? path : ENCODER.encode(path); if (path.length === 0) { return [this, current]; } const fork = this.forks.get(path[0]); if (fork && cafe_utility_1.Binary.commonPrefix(fork.prefix, path).length === fork.prefix.length) { return fork.node.findClosest(path.slice(fork.prefix.length), cafe_utility_1.Binary.concatBytes(current, fork.prefix)); } return [this, current]; } /** * Returns an array of all nodes in the tree which have a target address set. * * Must be called after `loadRecursively`. */ collect(nodes = []) { for (const fork of this.forks.values()) { if (!cafe_utility_1.Binary.equals(fork.node.targetAddress, __1.NULL_ADDRESS)) { nodes.push(fork.node); } fork.node.collect(nodes); } return nodes; } /** * Returns a path:reference map of all nodes in the tree which have a target address set. * * Must be called after `loadRecursively`. */ collectAndMap() { const nodes = this.collect(); const result = {}; for (const node of nodes) { result[node.fullPathString] = new typed_bytes_1.Reference(node.targetAddress).toHex(); } return result; } determineType() { let type = 0; if (!cafe_utility_1.Binary.equals(this.targetAddress, __1.NULL_ADDRESS) || cafe_utility_1.Binary.equals(this.path, PATH_SEPARATOR)) { type |= TYPE_VALUE; } if (this.forks.size > 0) { type |= TYPE_EDGE; } if (cafe_utility_1.Binary.indexOf(this.path, PATH_SEPARATOR) !== -1 && !cafe_utility_1.Binary.equals(this.path, PATH_SEPARATOR)) { type |= TYPE_WITH_PATH_SEPARATOR; } if (this.metadata) { type |= TYPE_WITH_METADATA; } return type; } } exports.MantarayNode = MantarayNode; function isType(value, type) { return (value & type) === type; }