refactor(kiwi): move FIG container helpers
This commit is contained in:
parent
d068237ee4
commit
8365112c51
|
|
@ -1,75 +1,7 @@
|
||||||
import { deflateSync, inflateSync } from 'fflate'
|
export {
|
||||||
|
buildFigKiwi,
|
||||||
export const FIG_KIWI_DEFAULT_VERSION = 101
|
decompressFigKiwiData,
|
||||||
|
decompressFigKiwiDataAsync,
|
||||||
export function parseFigKiwiChunks(binary: Uint8Array): Uint8Array[] | null {
|
FIG_KIWI_DEFAULT_VERSION,
|
||||||
const header = new TextDecoder().decode(binary.slice(0, 8))
|
parseFigKiwiChunks
|
||||||
if (header !== 'fig-kiwi') return null
|
} from '@open-pencil/kiwi/fig/container'
|
||||||
|
|
||||||
const view = new DataView(binary.buffer, binary.byteOffset, binary.byteLength)
|
|
||||||
let offset = 12
|
|
||||||
|
|
||||||
const chunks: Uint8Array[] = []
|
|
||||||
while (offset < binary.length) {
|
|
||||||
const chunkLen = view.getUint32(offset, true)
|
|
||||||
offset += 4
|
|
||||||
chunks.push(binary.slice(offset, offset + chunkLen))
|
|
||||||
offset += chunkLen
|
|
||||||
}
|
|
||||||
return chunks.length >= 2 ? chunks : null
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function decompressFigKiwiDataAsync(compressed: Uint8Array): Promise<Uint8Array> {
|
|
||||||
if (
|
|
||||||
compressed.length >= 4 &&
|
|
||||||
compressed[0] === 0x28 &&
|
|
||||||
compressed[1] === 0xb5 &&
|
|
||||||
compressed[2] === 0x2f &&
|
|
||||||
compressed[3] === 0xfd
|
|
||||||
) {
|
|
||||||
const { decompress } = await import('fzstd')
|
|
||||||
return decompress(compressed)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return inflateSync(compressed)
|
|
||||||
} catch {
|
|
||||||
const { decompress } = await import('fzstd')
|
|
||||||
return decompress(compressed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildFigKiwi(
|
|
||||||
schemaDeflated: Uint8Array,
|
|
||||||
dataRaw: Uint8Array,
|
|
||||||
version = FIG_KIWI_DEFAULT_VERSION
|
|
||||||
): Uint8Array {
|
|
||||||
let dataCompressed: Uint8Array
|
|
||||||
const zstdCompress: ((data: Uint8Array) => Uint8Array) | undefined = (() => {
|
|
||||||
const g = globalThis as { Bun?: { zstdCompressSync?: (data: Uint8Array) => Uint8Array } }
|
|
||||||
return g.Bun?.zstdCompressSync
|
|
||||||
})()
|
|
||||||
if (zstdCompress) {
|
|
||||||
dataCompressed = zstdCompress(dataRaw)
|
|
||||||
} else {
|
|
||||||
dataCompressed = deflateSync(dataRaw)
|
|
||||||
}
|
|
||||||
|
|
||||||
const total = 8 + 4 + 4 + schemaDeflated.length + 4 + dataCompressed.length
|
|
||||||
const out = new Uint8Array(total)
|
|
||||||
const view = new DataView(out.buffer)
|
|
||||||
|
|
||||||
out.set(new TextEncoder().encode('fig-kiwi'), 0)
|
|
||||||
view.setUint32(8, version, true)
|
|
||||||
|
|
||||||
let offset = 12
|
|
||||||
view.setUint32(offset, schemaDeflated.length, true)
|
|
||||||
offset += 4
|
|
||||||
out.set(schemaDeflated, offset)
|
|
||||||
offset += schemaDeflated.length
|
|
||||||
|
|
||||||
view.setUint32(offset, dataCompressed.length, true)
|
|
||||||
offset += 4
|
|
||||||
out.set(dataCompressed, offset)
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,12 @@
|
||||||
"import": "./dist/fig/codec.js",
|
"import": "./dist/fig/codec.js",
|
||||||
"default": "./dist/fig/codec.js"
|
"default": "./dist/fig/codec.js"
|
||||||
},
|
},
|
||||||
|
"./fig/container": {
|
||||||
|
"types": "./dist/src/fig/container.d.ts",
|
||||||
|
"bun": "./src/fig/container.ts",
|
||||||
|
"import": "./dist/fig/container.js",
|
||||||
|
"default": "./dist/fig/container.js"
|
||||||
|
},
|
||||||
"./fig/parse": {
|
"./fig/parse": {
|
||||||
"types": "./dist/src/fig/parse.d.ts",
|
"types": "./dist/src/fig/parse.d.ts",
|
||||||
"bun": "./src/fig/parse.ts",
|
"bun": "./src/fig/parse.ts",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { getSchemaBytes, initCodec, isCodecReady } from '../dist/fig/codec.js'
|
import { getSchemaBytes, initCodec, isCodecReady } from '../dist/fig/codec.js'
|
||||||
|
import { buildFigKiwi, parseFigKiwiChunks } from '../dist/fig/container.js'
|
||||||
import { parseFigKiwiContainer } from '../dist/fig/parse.js'
|
import { parseFigKiwiContainer } from '../dist/fig/parse.js'
|
||||||
import { figmaSchema, getKiwiMessageType, parseSchema, validateSchema } from '../dist/index.js'
|
import { figmaSchema, getKiwiMessageType, parseSchema, validateSchema } from '../dist/index.js'
|
||||||
|
|
||||||
|
|
@ -27,3 +28,8 @@ if (!isCodecReady() || getSchemaBytes().length === 0) {
|
||||||
if (parseFigKiwiContainer(new TextEncoder().encode('not-fig-kiwi')) !== null) {
|
if (parseFigKiwiContainer(new TextEncoder().encode('not-fig-kiwi')) !== null) {
|
||||||
throw new Error('Failed to reject invalid FIG Kiwi container')
|
throw new Error('Failed to reject invalid FIG Kiwi container')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const figKiwi = buildFigKiwi(new Uint8Array([1]), new Uint8Array([2]))
|
||||||
|
if (parseFigKiwiChunks(figKiwi)?.length !== 2) {
|
||||||
|
throw new Error('Failed to read built FIG Kiwi container chunks')
|
||||||
|
}
|
||||||
|
|
|
||||||
64
packages/kiwi/src/fig/container.ts
Normal file
64
packages/kiwi/src/fig/container.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { deflateSync, inflateSync } from 'fflate'
|
||||||
|
|
||||||
|
export const FIG_KIWI_DEFAULT_VERSION = 101
|
||||||
|
|
||||||
|
export function parseFigKiwiChunks(binary: Uint8Array): Uint8Array[] | null {
|
||||||
|
const header = new TextDecoder().decode(binary.slice(0, 8))
|
||||||
|
if (header !== 'fig-kiwi') return null
|
||||||
|
|
||||||
|
const view = new DataView(binary.buffer, binary.byteOffset, binary.byteLength)
|
||||||
|
let offset = 12
|
||||||
|
|
||||||
|
const chunks: Uint8Array[] = []
|
||||||
|
while (offset < binary.length) {
|
||||||
|
const chunkLen = view.getUint32(offset, true)
|
||||||
|
offset += 4
|
||||||
|
chunks.push(binary.slice(offset, offset + chunkLen))
|
||||||
|
offset += chunkLen
|
||||||
|
}
|
||||||
|
return chunks.length >= 2 ? chunks : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decompressFigKiwiData(compressed: Uint8Array): Uint8Array {
|
||||||
|
try {
|
||||||
|
return inflateSync(compressed)
|
||||||
|
} catch {
|
||||||
|
throw new Error('Failed to decompress fig-kiwi data')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decompressFigKiwiDataAsync(compressed: Uint8Array): Promise<Uint8Array> {
|
||||||
|
try {
|
||||||
|
return inflateSync(compressed)
|
||||||
|
} catch {
|
||||||
|
const fzstd = await import('fzstd')
|
||||||
|
return fzstd.decompress(compressed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildFigKiwi(
|
||||||
|
schemaDeflated: Uint8Array,
|
||||||
|
dataRaw: Uint8Array,
|
||||||
|
version = FIG_KIWI_DEFAULT_VERSION
|
||||||
|
): Uint8Array {
|
||||||
|
const dataDeflated = deflateSync(dataRaw)
|
||||||
|
|
||||||
|
const total = 8 + 4 + 4 + schemaDeflated.length + 4 + dataDeflated.length
|
||||||
|
const out = new Uint8Array(total)
|
||||||
|
const view = new DataView(out.buffer)
|
||||||
|
|
||||||
|
out.set(new TextEncoder().encode('fig-kiwi'), 0)
|
||||||
|
view.setUint32(8, version, true)
|
||||||
|
|
||||||
|
let offset = 12
|
||||||
|
view.setUint32(offset, schemaDeflated.length, true)
|
||||||
|
offset += 4
|
||||||
|
out.set(schemaDeflated, offset)
|
||||||
|
offset += schemaDeflated.length
|
||||||
|
|
||||||
|
view.setUint32(offset, dataDeflated.length, true)
|
||||||
|
offset += 4
|
||||||
|
out.set(dataDeflated, offset)
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
44
packages/kiwi/tests/container.test.ts
Normal file
44
packages/kiwi/tests/container.test.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { describe, expect, test } from 'bun:test'
|
||||||
|
|
||||||
|
import { deflateSync } from 'fflate'
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildFigKiwi,
|
||||||
|
decompressFigKiwiData,
|
||||||
|
decompressFigKiwiDataAsync,
|
||||||
|
FIG_KIWI_DEFAULT_VERSION,
|
||||||
|
parseFigKiwiChunks
|
||||||
|
} from '../src/fig/container'
|
||||||
|
|
||||||
|
describe('Figma FIG Kiwi container helpers', () => {
|
||||||
|
test('builds and parses fig-kiwi chunks', () => {
|
||||||
|
const schemaDeflated = new Uint8Array([1, 2, 3])
|
||||||
|
const dataRaw = new TextEncoder().encode('payload')
|
||||||
|
const binary = buildFigKiwi(schemaDeflated, dataRaw)
|
||||||
|
|
||||||
|
const chunks = parseFigKiwiChunks(binary)
|
||||||
|
|
||||||
|
expect(chunks).not.toBeNull()
|
||||||
|
expect(chunks?.[0]).toEqual(schemaDeflated)
|
||||||
|
expect(chunks?.[1]).toEqual(deflateSync(dataRaw))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('uses the default container version', () => {
|
||||||
|
const binary = buildFigKiwi(new Uint8Array([1]), new Uint8Array([2]))
|
||||||
|
const view = new DataView(binary.buffer, binary.byteOffset, binary.byteLength)
|
||||||
|
|
||||||
|
expect(view.getUint32(8, true)).toBe(FIG_KIWI_DEFAULT_VERSION)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('decompresses deflated data synchronously and asynchronously', async () => {
|
||||||
|
const dataRaw = new TextEncoder().encode('payload')
|
||||||
|
const deflated = deflateSync(dataRaw)
|
||||||
|
|
||||||
|
expect(decompressFigKiwiData(deflated)).toEqual(dataRaw)
|
||||||
|
await expect(decompressFigKiwiDataAsync(deflated)).resolves.toEqual(dataRaw)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('rejects non fig-kiwi data', () => {
|
||||||
|
expect(parseFigKiwiChunks(new TextEncoder().encode('not-fig-kiwi'))).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -21,6 +21,7 @@ export default defineConfig({
|
||||||
'schema-runtime': './src/schema-runtime/index.ts',
|
'schema-runtime': './src/schema-runtime/index.ts',
|
||||||
fig: './src/fig/index.ts',
|
fig: './src/fig/index.ts',
|
||||||
'fig/codec': './src/fig/codec.ts',
|
'fig/codec': './src/fig/codec.ts',
|
||||||
|
'fig/container': './src/fig/container.ts',
|
||||||
'fig/parse': './src/fig/parse.ts'
|
'fig/parse': './src/fig/parse.ts'
|
||||||
},
|
},
|
||||||
plugins: [rawText()],
|
plugins: [rawText()],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue