Files
start-os/sdk/base/lib/s9pk/index.ts
Aiden McClelland 855c1f1b07 style(sdk): apply prettier with single quotes
Run prettier across sdk/base and sdk/package to apply the
standardized quote style (single quotes matching web).
2026-02-05 13:34:01 -07:00

129 lines
3.6 KiB
TypeScript

import {
DataUrl,
DependencyMetadata,
Manifest,
MerkleArchiveCommitment,
PackageId,
} from '../osBindings'
import { ArrayBufferReader, MerkleArchive } from './merkleArchive'
import mime from 'mime'
import { DirectoryContents } from './merkleArchive/directoryContents'
import { FileContents } from './merkleArchive/fileContents'
const magicAndVersion = new Uint8Array([59, 59, 2])
export function compare(a: Uint8Array, b: Uint8Array) {
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false
}
return true
}
export class S9pk {
private constructor(
readonly manifest: Manifest,
readonly archive: MerkleArchive,
readonly size: number,
) {}
static async deserialize(
source: Blob,
commitment: MerkleArchiveCommitment | null,
): Promise<S9pk> {
const header = new ArrayBufferReader(
await source
.slice(0, magicAndVersion.length + MerkleArchive.headerSize)
.arrayBuffer(),
)
const magicVersion = new Uint8Array(header.next(magicAndVersion.length))
if (!compare(magicVersion, magicAndVersion)) {
throw new Error('Invalid Magic or Unexpected Version')
}
const archive = await MerkleArchive.deserialize(
source,
's9pk',
header,
commitment,
)
const manifest = JSON.parse(
new TextDecoder().decode(
await archive.contents
.getPath(['manifest.json'])
?.verifiedFileContents(),
),
)
return new S9pk(manifest, archive, source.size)
}
async icon(): Promise<DataUrl> {
const iconName = Object.keys(this.archive.contents.contents).find(
(name) =>
name.startsWith('icon.') && mime.getType(name)?.startsWith('image/'),
)
if (!iconName) {
throw new Error('no icon found in archive')
}
return (
`data:${mime.getType(iconName)};base64,` +
Buffer.from(
await this.archive.contents.getPath([iconName])!.verifiedFileContents(),
).toString('base64')
)
}
async dependencyMetadataFor(id: PackageId) {
const entry = this.archive.contents.getPath([
'dependencies',
id,
'metadata.json',
])
if (!entry) return null
return JSON.parse(
new TextDecoder().decode(await entry.verifiedFileContents()),
) as { title: string }
}
async dependencyIconFor(id: PackageId) {
const dir = this.archive.contents.getPath(['dependencies', id])
if (!dir || !(dir.contents instanceof DirectoryContents)) return null
const iconName = Object.keys(dir.contents.contents).find(
(name) =>
name.startsWith('icon.') && mime.getType(name)?.startsWith('image/'),
)
if (!iconName) return null
return (
`data:${mime.getType(iconName)};base64,` +
Buffer.from(
await dir.contents.getPath([iconName])!.verifiedFileContents(),
).toString('base64')
)
}
async dependencyMetadata() {
return Object.fromEntries(
await Promise.all(
Object.entries(this.manifest.dependencies)
.filter(([_, info]) => !!info)
.map(async ([id, info]) => [
id,
{
...(await this.dependencyMetadataFor(id)),
icon: await this.dependencyIconFor(id),
description: info!.description,
optional: info!.optional,
},
]),
),
)
}
async license(): Promise<string> {
const file = this.archive.contents.getPath(['LICENSE.md'])
if (!file || !(file.contents instanceof FileContents))
throw new Error('license.md not found in archive')
return new TextDecoder().decode(await file.verifiedFileContents())
}
}