@atproto/repo
Version:
atproto repo and MST implementation
146 lines (133 loc) • 3.56 kB
text/typescript
import { CID } from 'multiformats'
import * as uint8arrays from 'uint8arrays'
import { cidForCbor } from '@atproto/common'
import { sha256 } from '@atproto/crypto'
import { ReadableBlockstore } from '../storage'
import { Leaf, MST, MstOpts, NodeData, NodeEntry } from './mst'
export const leadingZerosOnHash = async (key: string | Uint8Array) => {
const hash = await sha256(key)
let leadingZeros = 0
for (let i = 0; i < hash.length; i++) {
const byte = hash[i]
if (byte < 64) leadingZeros++
if (byte < 16) leadingZeros++
if (byte < 4) leadingZeros++
if (byte === 0) {
leadingZeros++
} else {
break
}
}
return leadingZeros
}
export const layerForEntries = async (
entries: NodeEntry[],
): Promise<number | null> => {
const firstLeaf = entries.find((entry) => entry.isLeaf())
if (!firstLeaf || firstLeaf.isTree()) return null
return await leadingZerosOnHash(firstLeaf.key)
}
export const deserializeNodeData = async (
storage: ReadableBlockstore,
data: NodeData,
opts?: Partial<MstOpts>,
): Promise<NodeEntry[]> => {
const { layer } = opts || {}
const entries: NodeEntry[] = []
if (data.l !== null) {
entries.push(
await MST.load(storage, data.l, {
layer: layer ? layer - 1 : undefined,
}),
)
}
let lastKey = ''
for (const entry of data.e) {
const keyStr = uint8arrays.toString(entry.k, 'ascii')
const key = lastKey.slice(0, entry.p) + keyStr
ensureValidMstKey(key)
entries.push(new Leaf(key, entry.v))
lastKey = key
if (entry.t !== null) {
entries.push(
await MST.load(storage, entry.t, {
layer: layer ? layer - 1 : undefined,
}),
)
}
}
return entries
}
export const serializeNodeData = (entries: NodeEntry[]): NodeData => {
const data: NodeData = {
l: null,
e: [],
}
let i = 0
if (entries[0]?.isTree()) {
i++
data.l = entries[0].pointer
}
let lastKey = ''
while (i < entries.length) {
const leaf = entries[i]
const next = entries[i + 1]
if (!leaf.isLeaf()) {
throw new Error('Not a valid node: two subtrees next to each other')
}
i++
let subtree: CID | null = null
if (next?.isTree()) {
subtree = next.pointer
i++
}
ensureValidMstKey(leaf.key)
const prefixLen = countPrefixLen(lastKey, leaf.key)
data.e.push({
p: prefixLen,
k: uint8arrays.fromString(leaf.key.slice(prefixLen), 'ascii'),
v: leaf.value,
t: subtree,
})
lastKey = leaf.key
}
return data
}
export const countPrefixLen = (a: string, b: string): number => {
let i
for (i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
break
}
}
return i
}
export const cidForEntries = async (entries: NodeEntry[]): Promise<CID> => {
const data = serializeNodeData(entries)
return cidForCbor(data)
}
export const isValidMstKey = (str: string): boolean => {
const split = str.split('/')
return (
str.length <= 1024 &&
split.length === 2 &&
split[0].length > 0 &&
split[1].length > 0 &&
isValidChars(split[0]) &&
isValidChars(split[1])
)
}
export const validCharsRegex = /^[a-zA-Z0-9_~\-:.]*$/
export const isValidChars = (str: string): boolean => {
return str.match(validCharsRegex) !== null
}
export const ensureValidMstKey = (str: string) => {
if (!isValidMstKey(str)) {
throw new InvalidMstKeyError(str)
}
}
export class InvalidMstKeyError extends Error {
constructor(public key: string) {
super(`Not a valid MST key: ${key}`)
}
}