Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit e91222a

Browse files
committedJan 8, 2025·
Add validation for exported ActivityPub tarballs
1 parent 073a97b commit e91222a

8 files changed

+169
-2
lines changed
 

‎out/test-export-2024-01-01.tar

-7 KB
Binary file not shown.

‎src/verify.ts

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import * as tar from 'tar-stream'
2+
import { Readable } from 'stream'
3+
import YAML from 'yaml'
4+
5+
/**
6+
* Validates the structure and content of an exported ActivityPub tarball.
7+
* @param tarBuffer - A Buffer containing the .tar archive.
8+
* @returns A promise that resolves to an object with `valid` (boolean) and `errors` (string[]).
9+
*/
10+
export async function validateExportStream(
11+
tarBuffer: Buffer
12+
): Promise<{ valid: boolean; errors: string[] }> {
13+
const extract = tar.extract()
14+
const errors: string[] = []
15+
const requiredFiles = [
16+
'manifest.yaml',
17+
'activitypub/actor.json',
18+
'activitypub/outbox.json'
19+
]
20+
const foundFiles = new Set()
21+
22+
return await new Promise((resolve) => {
23+
extract.on('entry', (header, stream, next) => {
24+
const fileName = header.name
25+
foundFiles.add(fileName)
26+
27+
let content = ''
28+
stream.on('data', (chunk) => {
29+
content += chunk.toString()
30+
})
31+
32+
stream.on('end', () => {
33+
try {
34+
// Validate JSON files
35+
if (fileName.endsWith('.json')) {
36+
JSON.parse(content) // Throws an error if content is not valid JSON
37+
}
38+
39+
// Validate manifest file
40+
if (fileName === 'manifest.yaml') {
41+
const manifest = YAML.parse(content)
42+
if (!manifest['ubc-version']) {
43+
errors.push('Manifest is missing required field: ubc-version')
44+
}
45+
if (!manifest.contents?.activitypub) {
46+
errors.push(
47+
'Manifest is missing required field: contents.activitypub'
48+
)
49+
}
50+
}
51+
} catch (error: any) {
52+
errors.push(`Error processing file ${fileName}: ${error.message}`)
53+
}
54+
next()
55+
})
56+
57+
stream.on('error', (error) => {
58+
errors.push(`Stream error on file ${fileName}: ${error.message}`)
59+
next()
60+
})
61+
})
62+
63+
extract.on('finish', () => {
64+
// Check if all required files are present
65+
for (const file of requiredFiles) {
66+
if (!foundFiles.has(file)) {
67+
errors.push(`Missing required file: ${file}`)
68+
}
69+
}
70+
71+
resolve({
72+
valid: errors.length === 0,
73+
errors
74+
})
75+
})
76+
77+
extract.on('error', (error) => {
78+
errors.push(`Error during extraction: ${error.message}`)
79+
resolve({
80+
valid: false,
81+
errors
82+
})
83+
})
84+
85+
// Convert Buffer to a Readable stream and pipe it to the extractor
86+
const stream = Readable.from(tarBuffer)
87+
stream.pipe(extract)
88+
})
89+
}

‎test/fixtures/account2.tar

-42 KB
Binary file not shown.
9.84 KB
Binary file not shown.
Binary file not shown.
20.5 KB
Binary file not shown.

‎test/index.spec.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,15 @@ describe('exportActorProfile', () => {
3535
describe('importActorProfile', () => {
3636
it('extracts and verifies contents from account2.tar', async () => {
3737
// Load the tar file as a buffer
38-
const tarBuffer = fs.readFileSync('test/fixtures/account2.tar')
38+
const tarBuffer = fs.readFileSync(
39+
'test/fixtures/tarball-samples/valid-export.tar'
40+
)
3941

4042
// Use the importActorProfile function to parse the tar contents
4143
const importedData = await importActorProfile(tarBuffer)
4244

4345
// Log or inspect the imported data structure
44-
console.log('Imported Data:', importedData)
46+
// console.log('Imported Data:', importedData)
4547

4648
// Example assertions to check specific files and content
4749
expect(importedData).to.have.property('activitypub/actor.json')

‎test/verify.spec.ts

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { expect } from 'chai'
2+
import { readFileSync } from 'fs'
3+
import { validateExportStream } from '../src/verify'
4+
5+
describe('validateExportStream', () => {
6+
it('should validate a valid tarball', async () => {
7+
// Load a valid tarball (e.g., exported-profile-valid.tar)
8+
const tarBuffer = readFileSync(
9+
'test/fixtures/tarball-samples/valid-export.tar'
10+
)
11+
const result = await validateExportStream(tarBuffer)
12+
13+
expect(result.valid).to.be.true
14+
expect(result.errors).to.be.an('array').that.is.empty
15+
})
16+
17+
it('should fail if manifest.yaml is missing', async () => {
18+
// Load a tarball with missing manifest.yaml
19+
const tarBuffer = readFileSync(
20+
'test/fixtures/tarball-samples/missing-manifest.tar'
21+
)
22+
const result = await validateExportStream(tarBuffer)
23+
24+
expect(result.valid).to.be.false
25+
})
26+
27+
it('should fail if actor.json is missing', async () => {
28+
// Load a tarball with missing actor.json
29+
const tarBuffer = readFileSync(
30+
'test/fixtures/tarball-samples/missing-actor.tar'
31+
)
32+
const result = await validateExportStream(tarBuffer)
33+
34+
expect(result.valid).to.be.false
35+
console.log(JSON.stringify(result.errors))
36+
})
37+
38+
// it('should fail if outbox.json is missing', async () => {
39+
// // Load a tarball with missing outbox.json
40+
// const tarBuffer = readFileSync(
41+
// 'test/fixtures/exported-profile-missing-outbox.tar'
42+
// )
43+
// const result = await validateExportStream(tarBuffer)
44+
45+
// expect(result.valid).to.be.false
46+
// expect(result.errors).to.include(
47+
// 'Missing required file: activitypub/outbox.json'
48+
// )
49+
// })
50+
51+
// it('should fail if actor.json contains invalid JSON', async () => {
52+
// // Load a tarball with invalid JSON in actor.json
53+
// const tarBuffer = readFileSync(
54+
// 'test/fixtures/exported-profile-invalid-actor-json.tar'
55+
// )
56+
// const result = await validateExportStream(tarBuffer)
57+
58+
// expect(result.valid).to.be.false
59+
// expect(result.errors).to.include(
60+
// 'Error processing file activitypub/actor.json: Unexpected token } in JSON at position 42'
61+
// )
62+
// })
63+
64+
// it('should fail if manifest.yaml is invalid', async () => {
65+
// // Load a tarball with invalid manifest.yaml
66+
// const tarBuffer = readFileSync(
67+
// 'test/fixtures/exported-profile-invalid-manifest.tar'
68+
// )
69+
// const result = await validateExportStream(tarBuffer)
70+
71+
// expect(result.valid).to.be.false
72+
// expect(result.errors).to.include(
73+
// 'Manifest is missing required field: ubc-version'
74+
// )
75+
// })
76+
})

0 commit comments

Comments
 (0)
Please sign in to comment.