diff --git a/src/cid.ts b/src/cid.ts new file mode 100644 index 0000000..8ea03c8 --- /dev/null +++ b/src/cid.ts @@ -0,0 +1,112 @@ +import Multibase from "#multibase.js"; +import { Multihash } from "#multihash.js"; +import { CID_TYPES, REGISTRY_TYPES } from "#constants.js"; +import { decodeEndian, encodeEndian } from "#util.js"; +import { concatBytes, equalBytes } from "@noble/curves/abstract/utils"; + +export default class CID extends Multibase { + type: number; + hash: Multihash; + size?: number; + + constructor(type: number, hash: Multihash, size?: number) { + super(); + this.type = type; + this.hash = hash; + this.size = size; + } + + static decode(cid: string): CID { + const decodedBytes = Multibase.decodeString(cid); + return CID._init(decodedBytes); + } + + static fromBytes(bytes: Uint8Array): CID { + return CID._init(bytes); + } + + private static _init(bytes: Uint8Array): CID { + const type = bytes[0]; + if (type === CID_TYPES.BRIDGE) { + return new CID(type, new Multihash(bytes)); + } + + const hash = new Multihash(bytes.subarray(1, 34)); + let size: number | undefined = undefined; + const sizeBytes = bytes.subarray(34); + if (sizeBytes.length > 0) { + size = decodeEndian(sizeBytes); + } + + if (!Object.values(CID_TYPES).includes(type)) { + throw new Error(`invalid cid type ${type}`); + } + + return new CID(type, hash, size); + } + + copyWith({ size, type }: { type?: number; size?: number }): CID { + type = type || this.type; + + if (!Object.values(CID_TYPES).includes(type)) { + throw new Error(`invalid cid type ${type}`); + } + + return new CID(type, this.hash, size || this.size); + } + + toBytes(): Uint8Array { + if (this.type === CID_TYPES.BRIDGE) { + return this.hash.fullBytes; + } else if (this.type === CID_TYPES.RAW) { + let sizeBytes = encodeEndian(this.size as number, 8); + + while (sizeBytes.length > 0 && sizeBytes[sizeBytes.length - 1] === 0) { + sizeBytes = sizeBytes.slice(0, -1); + } + if (sizeBytes.length === 0) { + sizeBytes = new Uint8Array(1); + } + + return concatBytes( + this._getPrefixBytes(), + this.hash.fullBytes, + sizeBytes, + ); + } + + return concatBytes(this._getPrefixBytes(), this.hash.fullBytes); + } + + private _getPrefixBytes(): Uint8Array { + return Uint8Array.from([this.type]); + } + + toRegistryEntry(): Uint8Array { + return concatBytes(Uint8Array.from([REGISTRY_TYPES.CID]), this.toBytes()); + } + + toRegistryCID(): Uint8Array { + return this.copyWith({ type: CID_TYPES.RESOLVER }).toBytes(); + } + + toString(): string { + return this.type === CID_TYPES.BRIDGE + ? Buffer.from(this.hash.fullBytes).toString("utf8") + : this.toBase58(); + } + + equals(other: CID): boolean { + return equalBytes(this.toBytes(), other.toBytes()); + } + + hashCode(): number { + const fullBytes = this.toBytes(); + return ( + fullBytes[0] + + fullBytes[1] * 256 + + fullBytes[2] * 256 * 256 + + fullBytes[3] * 256 * 256 * 256 + ); + } +} diff --git a/src/index.ts b/src/index.ts index d8f10b9..ae426d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import { ed25519 } from "@noble/curves/ed25519"; import KeyPairEd25519 from "#ed25519.js"; import { S5NodeConfig } from "./types.js"; import NodeId from "#nodeId.js"; +import CID from "#cid.js"; export * from "./types.js"; export * from "./constants.js"; @@ -16,7 +17,7 @@ export { BasePeer, } from "./transports/index.js"; export type { SignedRegistryEntry }; -export { NodeId }; +export { NodeId, CID }; export function createNode(config: S5NodeConfig) { return new S5Node(config);