Skip to content

Commit 18b23d8

Browse files
committed
feat: basic lt code implement
1 parent 50efcf7 commit 18b23d8

File tree

2 files changed

+257
-0
lines changed

2 files changed

+257
-0
lines changed

test/lt.test.ts

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import fs from 'node:fs/promises'
2+
import { join } from 'node:path'
3+
4+
import { expect, it } from 'vitest'
5+
import { binaryToBlock, blockToBinary, createDecoder, encodeFountain } from '../utils/lt-codes'
6+
7+
it('slice binary', async () => {
8+
const input = (await fs.readFile(join('test', 'SampleJPGImage_100kbmb.jpg'), null)).buffer
9+
const data = new Uint8Array(input)
10+
11+
const decoder = createDecoder()
12+
let count = 0
13+
for (const block of encodeFountain(data, 1000)) {
14+
count += 1
15+
if (count > 1000)
16+
throw new Error('Too many blocks')
17+
const binary = blockToBinary(block)
18+
// Use the binary to transfer
19+
const back = binaryToBlock(binary)
20+
const result = decoder.addBlock([back])
21+
if (result)
22+
break
23+
}
24+
25+
const result = decoder.getDecoded()!
26+
expect(result).toBeDefined()
27+
expect(result).toBeInstanceOf(Uint8Array)
28+
expect(result.length).toBe(data.length)
29+
})

utils/lt-codes.ts

+228
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
interface EncodingMeta {
2+
/**
3+
* Number of original data blocks
4+
*/
5+
k: number
6+
/**
7+
* Data length
8+
*/
9+
length: number
10+
/**
11+
* Checksum
12+
*/
13+
sum: number
14+
}
15+
16+
interface EncodingBlock extends EncodingMeta {
17+
indices: number[]
18+
data: Uint8Array
19+
}
20+
21+
export function blockToBinary(block: EncodingBlock): Uint8Array {
22+
const { k, length, sum, indices, data } = block
23+
const header = new Uint32Array([
24+
indices.length,
25+
...indices,
26+
k,
27+
length,
28+
sum,
29+
])
30+
const binary = new Uint8Array(header.length * 4 + data.length)
31+
let offset = 0
32+
binary.set(new Uint8Array(header.buffer), offset)
33+
offset += header.length * 4
34+
binary.set(data, offset)
35+
return binary
36+
}
37+
38+
export function binaryToBlock(binary: Uint8Array): EncodingBlock {
39+
const degree = new Uint32Array(binary.buffer, 0, 4)[0]!
40+
const headerRest = Array.from(new Uint32Array(binary.buffer, 4, degree + 3))
41+
const indices = headerRest.slice(0, degree)
42+
const [
43+
k,
44+
length,
45+
sum,
46+
] = headerRest.slice(degree) as [number, number, number]
47+
const data = binary.slice(4 * (degree + 4))
48+
return {
49+
k,
50+
length,
51+
sum,
52+
indices,
53+
data,
54+
}
55+
}
56+
57+
// CRC32 checksum
58+
function checksum(data: Uint8Array): number {
59+
let crc = 0xFFFFFFFF
60+
for (let i = 0; i < data.length; i++) {
61+
crc = crc ^ data[i]!
62+
for (let j = 0; j < 8; j++) {
63+
crc = crc & 1 ? (crc >>> 1) ^ 0xEDB88320 : crc >>> 1
64+
}
65+
}
66+
return crc ^ 0xFFFFFFFF
67+
}
68+
69+
// Use Ideal Soliton Distribution to select degree
70+
function getRandomDegree(k: number): number {
71+
const probability = Math.random()
72+
let degree = 1
73+
74+
for (let d = 2; d <= k; d++) {
75+
const prob = 1 / (d * (d - 1))
76+
if (probability < prob) {
77+
degree = d
78+
break
79+
}
80+
}
81+
82+
return degree
83+
}
84+
85+
// Randomly select indices of degree number of original data blocks
86+
function getRandomIndices(k: number, degree: number): number[] {
87+
const indices: Set<number> = new Set()
88+
while (indices.size < degree) {
89+
const randomIndex = Math.floor(Math.random() * k)
90+
indices.add(randomIndex)
91+
}
92+
return Array.from(indices)
93+
}
94+
95+
function xorUint8Array(a: Uint8Array, b: Uint8Array): Uint8Array {
96+
const result = new Uint8Array(a.length)
97+
for (let i = 0; i < a.length; i++) {
98+
result[i] = a[i]! ^ b[i]!
99+
}
100+
return result
101+
}
102+
103+
function sliceData(data: Uint8Array, blockSize: number): Uint8Array[] {
104+
const blocks: Uint8Array[] = []
105+
for (let i = 0; i < data.length; i += blockSize) {
106+
const block = new Uint8Array(blockSize)
107+
block.set(data.slice(i, i + blockSize))
108+
blocks.push(block)
109+
}
110+
return blocks
111+
}
112+
113+
export function *encodeFountain(data: Uint8Array, indiceSize: number): Generator<EncodingBlock> {
114+
const sum = checksum(data)
115+
const indices = sliceData(data, indiceSize)
116+
const k = indices.length
117+
const meta: EncodingMeta = {
118+
k,
119+
length: data.length,
120+
sum,
121+
}
122+
123+
while (true) {
124+
const degree = getRandomDegree(k)
125+
const selectedIndices = getRandomIndices(k, degree)
126+
let encodedData = new Uint8Array(indiceSize)
127+
128+
for (const index of selectedIndices) {
129+
encodedData = xorUint8Array(encodedData, indices[index]!)
130+
}
131+
132+
yield {
133+
...meta,
134+
indices: selectedIndices,
135+
data: encodedData,
136+
}
137+
}
138+
}
139+
140+
export function createDecoder(blocks?: EncodingBlock[]) {
141+
return new LtDecoder(blocks)
142+
}
143+
144+
export class LtDecoder {
145+
public decodedData: (Uint8Array | undefined)[] = []
146+
public decodedCount = 0
147+
public encodedBlocks: Set<EncodingBlock> = new Set()
148+
public meta: EncodingBlock = undefined!
149+
150+
constructor(blocks?: EncodingBlock[]) {
151+
if (blocks) {
152+
this.addBlock(blocks)
153+
}
154+
}
155+
156+
// Add blocks and decode them on the fly
157+
addBlock(blocks: EncodingBlock[]): boolean {
158+
if (!blocks.length) {
159+
return false
160+
}
161+
162+
if (!this.meta) {
163+
this.meta = blocks[0]!
164+
}
165+
166+
for (const block of blocks) {
167+
this.encodedBlocks.add(block)
168+
}
169+
170+
for (const block of this.encodedBlocks) {
171+
let { data, indices } = block
172+
173+
for (const index of indices) {
174+
if (this.decodedData[index] != null) {
175+
data = xorUint8Array(data, this.decodedData[index]!)
176+
indices = indices.filter(i => i !== index)
177+
}
178+
}
179+
180+
block.data = data
181+
block.indices = indices
182+
183+
if (indices.length === 1) {
184+
this.decodedData[indices[0]!] = data
185+
this.decodedCount++
186+
this.encodedBlocks.delete(block)
187+
}
188+
}
189+
190+
return this.decodedCount === this.meta.k
191+
}
192+
193+
getDecoded(): Uint8Array | undefined {
194+
if (this.decodedCount !== this.meta.k) {
195+
return
196+
}
197+
if (this.decodedData.some(block => block == null)) {
198+
return
199+
}
200+
const indiceSize = this.meta.data.length
201+
const blocks = this.decodedData as Uint8Array[]
202+
const decodedData = new Uint8Array(this.meta.length)
203+
blocks.forEach((block, i) => {
204+
const start = i * indiceSize
205+
if (start + indiceSize > decodedData.length) {
206+
for (let j = 0; j < decodedData.length - start; j++) {
207+
decodedData[start + j] = block[j]!
208+
}
209+
}
210+
else {
211+
decodedData.set(block, i * indiceSize)
212+
}
213+
})
214+
const sum = checksum(decodedData)
215+
if (sum !== this.meta.sum) {
216+
throw new Error('Checksum mismatch')
217+
}
218+
return decodedData
219+
}
220+
}
221+
222+
export function tringToUint8Array(str: string): Uint8Array {
223+
const data = new Uint8Array(str.length)
224+
for (let i = 0; i < str.length; i++) {
225+
data[i] = str.charCodeAt(i)
226+
}
227+
return data
228+
}

0 commit comments

Comments
 (0)