char/sorcery
static-files based git repo viewer
git clone https://git.t4t.associates/char/sorcery
42f80d8
main
1import { inflate , oidBytes , toHex } from "./codec.ts" ; 2import type { Fetcher , GitRepo } from "./repo.ts" ; 3import type { GitObject , ObjectType } from "./types.ts" ; 4 5/** pack entry types (`OBJ_*` in git) */ 6const TYPES :Record < number , ObjectType > = { 1 :"commit" , 2 :"tree" , 3 :"blob" , 4 :"tag" }; 7const OFS_DELTA = 6 ; 8const REF_DELTA = 7 ; 9 10export interface PackIndex { 11lookup ( oid :string ) :Promise < number | null >; 12entryEnd ( offset :number ) :number | null ; 13} 14 15const PACK_INDEX_HEADER_BYTES = 8 + 256 * 4 ; 16 17interface PackIndexHeader { 18count :number ; 19fanout :number []; 20} 21 22function parseIndexHeader ( buf :Uint8Array ) :PackIndexHeader { 23const view = new DataView ( buf . buffer , buf . byteOffset , buf . byteLength ); 24if ( buf . length < PACK_INDEX_HEADER_BYTES || view . getUint32 ( 0 ) !== 0xff744f63 || view . getUint32 ( 4 ) !== 2 ) { 25throw new Error ( "unsupported pack index format" ); 26} 27const fanout = Array . from ({ length :256 }, ( _ , i ) => view . getUint32 ( 8 + i * 4 )); 28return { count :fanout [ 255 ], fanout}; 29} 30 31/** Eager path for small version-2 indexes. */ 32export function parseIndex ( buf :Uint8Array , hashBytes :number ) :PackIndex { 33const { count, fanout} = parseIndexHeader ( buf ); 34const view = new DataView ( buf . buffer , buf . byteOffset , buf . byteLength ); 35const names = PACK_INDEX_HEADER_BYTES ; 36const offsets = names + count * hashBytes + count * 4 ; // skip crc table 37const largeOffsets = offsets + count * 4 ; 38 39const oidAt = ( i :number ) => buf . subarray ( names + i * hashBytes , names + ( i + 1 ) * hashBytes ); 40const offsetAt = ( i :number ) :number => { 41const raw = view . getUint32 ( offsets + i * 4 ); 42if ( raw & 0x80000000 ) { 43return Number ( view . getBigUint64 ( largeOffsets + ( raw & 0x7fffffff ) * 8 )); 44} 45return raw ; 46}; 47 48const sortedOffsets = Array . from ({ length :count }, ( _ , i ) => offsetAt ( i )). sort (( a , b ) => a - b ); 49 50return { 51async lookup ( oid :string ) :Promise < number | null > { 52const target = oidBytes ( oid ); 53if ( target . length !== hashBytes ) throw new Error ( `object id ${ oid } has the wrong hash format` ); 54let lo = target [ 0 ] === 0 ?0 :fanout [ target [ 0 ] - 1 ]; 55let hi = fanout [ target [ 0 ]]; 56while ( lo < hi ) { 57const mid = ( lo + hi ) >> 1 ; 58const cmp = compare ( oidAt ( mid ), target ); 59if ( cmp === 0 ) return offsetAt ( mid ); 60if ( cmp < 0 ) lo = mid + 1 ; 61else hi = mid ; 62} 63return null ; 64}, 65entryEnd ( offset :number ) :number | null { 66const i = sortedOffsets . findIndex ( candidate => candidate > offset ); 67return i === - 1 ?null :sortedOffsets [ i ]; 68}, 69}; 70} 71 72export function compare ( a :Uint8Array , b :Uint8Array ) :number { 73for ( let i = 0 ; i < a . length ; i ++ ) { 74if ( a [ i ] !== b [ i ]) return a [ i ] - b [ i ]; 75} 76return 0 ; 77} 78 79export interface PackEntry { 80type :number ; 81/** inflated payload: object content, or delta instructions */ 82data :Uint8Array ; 83/** for OFS_DELTA: absolute offset of the base entry */ 84baseOffset ?:number ; 85/** for REF_DELTA: oid of the base object */ 86baseOid ?:string ; 87} 88 89export interface PackEntryHeader { 90type :number ; 91size :number ; 92bytes :number ; 93baseOffset ?:number ; 94baseOid ?:string ; 95} 96 97export function parseEntryHeader ( slice :Uint8Array , hashBytes :number , entryOffset = 0 ) :PackEntryHeader { 98let pos = 0 ; 99let byte = slice [ pos ++ ]; 100const type = ( byte >> 4 ) & 7 ; 101let size = byte & 0x0f ; 102let shift = 4 ; 103while ( byte & 0x80 ) { 104if ( pos >= slice . length ) throw new Error ( "truncated pack entry header" ); 105byte = slice [ pos ++ ]; 106size += ( byte & 0x7f ) * 2 ** shift ; 107shift += 7 ; 108} 109 110let baseOffset :number | undefined ; 111let baseOid :string | undefined ; 112if ( type === OFS_DELTA ) { 113byte = slice [ pos ++ ]; 114let relative = byte & 0x7f ; 115while ( byte & 0x80 ) { 116byte = slice [ pos ++ ]; 117relative = (( relative + 1 ) << 7 ) | ( byte & 0x7f ); 118} 119baseOffset = entryOffset - relative ; 120} else if ( type === REF_DELTA ) { 121baseOid = toHex ( slice . subarray ( pos , pos + hashBytes )); 122pos += hashBytes ; 123} 124 125return { type, size, bytes :pos , baseOffset, baseOid}; 126} 127 128/** decode one entry from an exact pack slice starting at its header */ 129export async function parseEntry ( slice :Uint8Array , hashBytes :number , entryOffset :number ) :Promise < PackEntry > { 130const header = parseEntryHeader ( slice , hashBytes , entryOffset ); 131return { 132type :header . type , 133data :await inflate ( slice . subarray ( header . bytes )), 134baseOffset :header . baseOffset , 135baseOid :header . baseOid , 136}; 137} 138 139export function entryType ( type :number ) :ObjectType { 140const known = TYPES [ type ]; 141if ( ! known ) throw new Error ( `unexpected pack entry type ${ type } ` ); 142return known ; 143} 144 145export function isDelta ( type :number ) :boolean { 146return type === OFS_DELTA || type === REF_DELTA ; 147} 148 149export function deltaResultSize ( delta :Uint8Array ) :number { 150let pos = 0 ; 151const varint = () => { 152let value = 0 ; 153let shift = 0 ; 154let byte ; 155do { 156byte = delta [ pos ++ ]; 157value += ( byte & 0x7f ) * 2 ** shift ; 158shift += 7 ; 159} while ( byte & 0x80 ); 160return value ; 161}; 162varint (); 163return varint (); 164} 165 166/** apply copy/insert delta instructions to a base object */ 167export function applyDelta ( base :Uint8Array , delta :Uint8Array ) :Uint8Array { 168let pos = 0 ; 169const varint = () => { 170let value = 0 ; 171let shift = 0 ; 172let byte ; 173do { 174byte = delta [ pos ++ ]; 175value |= ( byte & 0x7f ) << shift ; 176shift += 7 ; 177} while ( byte & 0x80 ); 178return value >>> 0 ; 179}; 180 181const srcSize = varint (); 182if ( srcSize !== base . length ) throw new Error ( "delta base size mismatch" ); 183const dstSize = varint (); 184const out = new Uint8Array ( dstSize ); 185let outPos = 0 ; 186 187while ( pos < delta . length ) { 188const op = delta [ pos ++ ]; 189if ( op & 0x80 ) { 190// copy from base: bitmask selects which offset/size bytes follow 191let offset = 0 ; 192let size = 0 ; 193for ( let i = 0 ; i < 4 ; i ++ ) if ( op & ( 1 << i )) offset |= delta [ pos ++ ] << ( i * 8 ); 194for ( let i = 0 ; i < 3 ; i ++ ) if ( op & ( 0x10 << i )) size |= delta [ pos ++ ] << ( i * 8 ); 195if ( size === 0 ) size = 0x10000 ; 196out . set ( base . subarray ( offset , offset + size ), outPos ); 197outPos += size ; 198} else if ( op !== 0 ) { 199out . set ( delta . subarray ( pos , pos + op ), outPos ); 200pos += op ; 201outPos += op ; 202} else { 203throw new Error ( "invalid delta opcode 0" ); 204} 205} 206if ( outPos !== dstSize ) throw new Error ( "delta output size mismatch" ); 207return out ; 208} 209 210export class Pack { 211 #index?:Promise < PackIndex >; 212readonly #hashBytes:number ; 213 214constructor ( 215readonly fetch :Fetcher , 216readonly stem :string , 217) { 218const hash = stem . match ( / ^pack-([0-9a-f]{40}|[0-9a-f]{64})$ / )?.[ 1 ]; 219if ( ! hash ) throw new Error ( `invalid pack name ${ stem } ` ); 220this . #hashBytes= hash . length / 2 ; 221} 222 223index () :Promise < PackIndex > { 224if ( this . #index) return this . #index; 225this . #index= this . fetch ( `.git/objects/pack/ ${ this . stem } .idx` ). then ( result => { 226if ( ! result ) throw new Error ( `missing pack index ${ this . stem } ` ); 227return parseIndex ( result . bytes , this . #hashBytes); 228}); 229this . #index. catch (() => ( this . #index= undefined )); 230return this . #index; 231} 232 233/** read + resolve (possibly delta-chained) object content at an offset */ 234async readAt ( offset :number , repo :GitRepo ) :Promise < GitObject > { 235const entry = await this . #readPackedEntry( offset ); 236if ( ! isDelta ( entry . type )) { 237return { type :entryType ( entry . type ), data :entry . data }; 238} 239const base = entry . baseOffset !== undefined 240 ?await this . readAt ( entry . baseOffset , repo ) 241 :await repo . object ( entry . baseOid ! ); 242return { type :base . type , data :applyDelta ( base . data , entry . data ) }; 243} 244 245async #readPackedEntry( offset :number ) :Promise < PackEntry > { 246const index = await this . index (); 247const path = `.git/objects/pack/ ${ this . stem } .pack` ; 248const end = index . entryEnd ( offset ); 249const res = await this . fetch ( path , [ offset , end ]); 250if ( ! res ) throw new Error ( `missing pack ${ this . stem } ` ); 251let bytes = res . bytes ; 252if ( end === null ) bytes = bytes . subarray ( 0 , res . total - this . #hashBytes- offset ); 253return parseEntry ( bytes , this . #hashBytes, offset ); 254} 255}