import base64url from "base64url"; import * as fs from "fs"; import { PathLike } from "fs"; import { byteArrayToLong } from "../src/utils"; import { tagsParser } from "../src/parser"; import { BundleItem } from "../src/BundleItem"; import { deepHash } from "../src"; import { stringToBuffer } from "arweave/web/lib/utils"; import Arweave from "arweave"; import { promisify } from "util"; import { indexToType, Signer } from "../src/signing"; import axios, { AxiosResponse } from "axios"; import { SIG_CONFIG } from "../src/constants"; const write = promisify(fs.write); const read = promisify(fs.read); export default class FileDataItem implements BundleItem { public readonly filename: PathLike; async signatureLength(): Promise<number> { const length = SIG_CONFIG[await this.signatureType()]?.sigLength; if (!length) throw new Error("Signature type not supported"); return length; } async ownerLength(): Promise<number> { const length = SIG_CONFIG[await this.signatureType()]?.pubLength; if (!length) throw new Error("Signature type not supported"); return length; } constructor(filename: PathLike, id?: Buffer) { this.filename = filename; this._id = id; } private _id?: Buffer; get id(): string { return base64url.encode(this._id); } get rawId(): Buffer { if (this._id) { return this._id; } throw new Error("ID is not set"); } set rawId(id: Buffer) { this._id = id; } static isDataItem(obj: any): boolean { return obj.filename && typeof obj.filename === "string"; } static async verify(filename: PathLike): Promise<boolean> { const handle = await fs.promises.open(filename, "r"); const item = new FileDataItem(filename); const sigType = await item.signatureType(); const tagsStart = await item.getTagsStart(); const numberOfTags = await read( handle.fd, Buffer.allocUnsafe(8), 0, 8, tagsStart, ).then((r) => byteArrayToLong(r.buffer)); const numberOfTagsBytes = await read( handle.fd, Buffer.allocUnsafe(8), 0, 8, tagsStart + 8, ).then((r) => byteArrayToLong(r.buffer)); if (numberOfTagsBytes > 4096) { await handle.close(); return false; } const tagsBytes = await read( handle.fd, Buffer.allocUnsafe(numberOfTagsBytes), 0, numberOfTagsBytes, tagsStart + 16, ).then((r) => r.buffer); if (numberOfTags > 0) { try { tagsParser.fromBuffer(tagsBytes); } catch (e) { await handle.close(); return false; } } const Signer = indexToType[sigType]; const owner = await item.rawOwner(); const signatureData = await deepHash([ stringToBuffer("dataitem"), stringToBuffer("1"), stringToBuffer(sigType.toString()), owner, await item.rawTarget(), await item.rawAnchor(), await item.rawTags(), fs.createReadStream(filename, { start: await item.dataStart(), }), ]); const sig = await item.rawSignature(); if (!(await Signer.verify(owner, signatureData, sig))) { await handle.close(); return false; } await handle.close(); return true; } isValid(): Promise<boolean> { return FileDataItem.verify(this.filename); } isSigned(): boolean { return this._id !== undefined; } async size(): Promise<number> { return await fs.promises.stat(this.filename).then((r) => r.size); } async signatureType(): Promise<number> { const handle = await fs.promises.open(this.filename, "r"); const buffer = await read(handle.fd, Buffer.allocUnsafe(2), 0, 2, 0).then( (r) => r.buffer, ); await handle.close(); return byteArrayToLong(buffer); } async rawSignature(): Promise<Buffer> { const handle = await fs.promises.open(this.filename, "r"); const length = await this.signatureLength(); const buffer = await read( handle.fd, Buffer.alloc(length), 0, length, 2, ).then((r) => r.buffer); await handle.close(); return buffer; } async signature(): Promise<string> { return base64url.encode(await this.rawSignature()); } async rawOwner(): Promise<Buffer> { const handle = await fs.promises.open(this.filename, "r"); const length = await this.ownerLength(); const buffer = await read( handle.fd, Buffer.allocUnsafe(length), 0, length, 2 + (await this.signatureLength()), ).then((r) => r.buffer); await handle.close(); return buffer; } async owner(): Promise<string> { return base64url.encode(await this.rawOwner()); } async rawTarget(): Promise<Buffer> { const handle = await fs.promises.open(this.filename, "r"); const targetStart = await this.getTargetStart(); const targetPresentBuffer = await read( handle.fd, Buffer.allocUnsafe(1), 0, 1, targetStart, ).then((r) => r.buffer); const targetPresent = targetPresentBuffer[0] === 1; if (targetPresent) { const targetBuffer = await read( handle.fd, Buffer.allocUnsafe(32), 0, 32, targetStart + 1, ).then((r) => r.buffer); await handle.close(); return targetBuffer; } await handle.close(); return Buffer.allocUnsafe(0); } async target(): Promise<string> { return base64url.encode(await this.rawTarget()); } async getTargetStart(): Promise<number> { return 2 + (await this.signatureLength()) + (await this.ownerLength()); } async rawAnchor(): Promise<Buffer> { const [anchorPresent, anchorStart] = await this.anchorStart(); if (anchorPresent) { const handle = await fs.promises.open(this.filename, "r"); const anchorBuffer = await read( handle.fd, Buffer.allocUnsafe(32), 0, 32, anchorStart + 1, ).then((r) => r.buffer); await handle.close(); return anchorBuffer; } return Buffer.allocUnsafe(0); } async anchor(): Promise<string> { return (await this.rawAnchor()).toString(); } async rawTags(): Promise<Buffer> { const handle = await fs.promises.open(this.filename, "r"); const tagsStart = await this.getTagsStart(); const numberOfTagsBuffer = await read( handle.fd, Buffer.allocUnsafe(8), 0, 8, tagsStart, ).then((r) => r.buffer); const numberOfTags = byteArrayToLong(numberOfTagsBuffer); if (numberOfTags === 0) { await handle.close(); return Buffer.allocUnsafe(0); } const numberOfTagsBytesBuffer = await read( handle.fd, Buffer.allocUnsafe(8), 0, 8, tagsStart + 8, ).then((r) => r.buffer); const numberOfTagsBytes = byteArrayToLong(numberOfTagsBytesBuffer); if (numberOfTagsBytes > 4096) { await handle.close(); throw new Error("Tags too large"); } const tagsBytes = await read( handle.fd, Buffer.allocUnsafe(numberOfTagsBytes), 0, numberOfTagsBytes, tagsStart + 16, ).then((r) => r.buffer); await handle.close(); return tagsBytes; } async tags(): Promise<{ name: string; value: string }[]> { const tagsBytes = await this.rawTags(); if (tagsBytes.byteLength === 0) return []; return tagsParser.fromBuffer(tagsBytes); } async rawData(): Promise<Buffer> { const dataStart = await this.dataStart(); const size = await this.size(); const dataSize = size - dataStart; if (dataSize === 0) { return Buffer.allocUnsafe(0); } const handle = await fs.promises.open(this.filename, "r"); const dataBuffer = await read( handle.fd, Buffer.allocUnsafe(dataSize), 0, dataSize, dataStart, ).then((r) => r.buffer); await handle.close(); return dataBuffer; } async data(): Promise<string> { return base64url.encode(await this.rawData()); } async sign(signer: Signer): Promise<Buffer> { const dataStart = await this.dataStart(); const signatureData = await deepHash([ stringToBuffer("dataitem"), stringToBuffer("1"), stringToBuffer((await this.signatureType()).toString()), await this.rawOwner(), await this.rawTarget(), await this.rawAnchor(), await this.rawTags(), fs.createReadStream(this.filename, { start: dataStart }), ]); const signatureBytes = await signer.sign(signatureData); const idBytes = await Arweave.crypto.hash(signatureBytes); const handle = await fs.promises.open(this.filename, "r+"); await write(handle.fd, signatureBytes, 0, await this.signatureLength(), 2); this.rawId = Buffer.from(idBytes); await handle.close(); return Buffer.from(idBytes); } /** * @deprecated Since version 0.3.0. Will be deleted in version 0.4.0. Use @bundlr-network/client package instead to interact with Bundlr */ public async sendToBundler(bundler: string): Promise<AxiosResponse> { const headers = { "Content-Type": "application/octet-stream", }; if (!this.isSigned()) throw new Error("You must sign before sending to bundler"); const response = await axios.post( `${bundler}/tx`, fs.createReadStream(this.filename), { headers, timeout: 100000, maxBodyLength: Infinity, validateStatus: (status) => (status > 200 && status < 300) || status !== 402, }, ); if (response.status === 402) throw new Error("Not enough funds to send data"); return response; } public async getTagsStart(): Promise<number> { const [anchorPresent, anchorStart] = await this.anchorStart(); let tagsStart = anchorStart; tagsStart += anchorPresent ? 33 : 1; return tagsStart; } public async dataStart(): Promise<number> { const handle = await fs.promises.open(this.filename, "r"); const tagsStart = await this.getTagsStart(); const numberOfTagsBytesBuffer = await read( handle.fd, Buffer.allocUnsafe(8), 0, 8, tagsStart + 8, ).then((r) => r.buffer); const numberOfTagsBytes = byteArrayToLong(numberOfTagsBytesBuffer); await handle.close(); return tagsStart + 16 + numberOfTagsBytes; } private async anchorStart(): Promise<[boolean, number]> { const targetStart = await this.getTargetStart(); const handle = await fs.promises.open(this.filename, "r"); const targetPresentBuffer = await read( handle.fd, Buffer.allocUnsafe(1), 0, 1, targetStart, ).then((r) => r.buffer); const targetPresent = targetPresentBuffer[0] === 1; const anchorStart = targetStart + (targetPresent ? 33 : 1); const anchorPresentBuffer = await read( handle.fd, Buffer.allocUnsafe(1), 0, 1, anchorStart, ).then((r) => r.buffer); const anchorPresent = anchorPresentBuffer[0] === 1; await handle.close(); return [anchorPresent, anchorStart]; } }