tar.mjs 5.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. import assert from 'node:assert/strict'
  2. import fs from 'node:fs'
  3. import zlib from 'node:zlib'
  4. /** Read regular package files without extracting an untrusted archive. */
  5. export function packageFiles(filename) {
  6. const data = zlib.gunzipSync(fs.readFileSync(filename), { maxOutputLength: 64 * 1024 * 1024 })
  7. const files = new Map()
  8. const names = new Set()
  9. let offset = 0, pending
  10. while (offset + 512 <= data.length) {
  11. const header = data.subarray(offset, offset + 512)
  12. if (header.every(byte => byte === 0)) break
  13. const field = (start, size) => header.toString('utf8', start, start + size).replace(/\0[\s\S]*$/, '')
  14. const size = parseInt(field(124, 12), 8) || 0
  15. assert.ok(Number.isSafeInteger(size) && size >= 0 && offset + 512 + size <= data.length, 'Invalid tar entry size')
  16. const type = field(156, 1)
  17. const body = data.subarray(offset + 512, offset + 512 + size)
  18. if (type === 'x') {
  19. const pax = body.toString('utf8')
  20. assert.ok(!/\blinkpath=/.test(pax), 'Package links are forbidden')
  21. pending = /(?:^|\n)\d+ path=([^\n]*)\n/.exec(pax)?.[1]
  22. } else if (type === 'L') pending = body.toString('utf8').replace(/\0[\s\S]*$/, '')
  23. else {
  24. assert.ok(['', '0', '5'].includes(type), `Package entry type is not permitted: ${type}`)
  25. const prefix = field(345, 155)
  26. const name = pending ?? (prefix ? `${prefix}/${field(0, 100)}` : field(0, 100))
  27. pending = undefined
  28. assert.ok(name.startsWith('package/') && !name.includes('\\') && !name.includes(':') && !name.split('/').some(part => part === '..' || part === '.'), `Unsafe package entry: ${name}`)
  29. if (type !== '5') {
  30. const relative = name.slice(8)
  31. assert.ok(relative && !names.has(relative.toLowerCase()), `Duplicate package entry: ${name}`)
  32. names.add(relative.toLowerCase())
  33. files.set(relative, body)
  34. }
  35. }
  36. offset += 512 + Math.ceil(size / 512) * 512
  37. }
  38. assert.ok(files.size, 'Empty package')
  39. return files
  40. }
  41. export function checkPublishableManifest(manifest) {
  42. assert.notEqual(manifest.private, true, 'Release packages must not be private')
  43. assert.equal(manifest.publishConfig?.access, 'public')
  44. assert.equal(manifest.publishConfig?.tag, 'preview')
  45. assert.equal(manifest.publishConfig?.registry, 'https://registry.npmjs.org/')
  46. for (const hook of ['preinstall', 'install', 'postinstall', 'prepare', 'prepublishOnly', 'prepack', 'postpack', 'publish', 'postpublish']) {
  47. assert.equal(manifest.scripts?.[hook], undefined, `Release packages must not run ${hook}`)
  48. }
  49. for (const [name, version] of Object.entries({ ...manifest.dependencies, ...manifest.optionalDependencies, ...manifest.peerDependencies })) {
  50. assert.ok(!name.startsWith('@webnovel/') && !/^(workspace:|file:|link:)/.test(version), 'Unpublished runtime dependency')
  51. }
  52. }
  53. export function checkEmbeddingPackage(filename, expectedVersion) {
  54. const files = packageFiles(filename)
  55. const read = file => { assert.ok(files.has(file), `Missing ${file}`); return files.get(file).toString('utf8') }
  56. const manifest = JSON.parse(read('package.json'))
  57. assert.equal(manifest.name, 'webnovel-embedding-provider')
  58. assert.equal(manifest.version, expectedVersion)
  59. assert.equal(manifest.license, 'GPL-3.0-only')
  60. checkPublishableManifest(manifest)
  61. assert.ok(read('LICENSE').includes('GNU GENERAL PUBLIC LICENSE'))
  62. assert.ok(read('THIRD_PARTY_NOTICES.md').includes('Original notices'))
  63. for (const name of ['lib/index.js', 'lib/client.js', 'cordis.patch.yml', 'README.md', 'MODEL_DIMENSIONS.md']) read(name)
  64. assert.ok(read('lib/client.js').startsWith(`window.__ModuleLoader__.load({ id: ${JSON.stringify(manifest.name)},`), 'Embedding client identity must match its installed package name')
  65. for (const [file, data] of files) {
  66. assert.match(file, /^(package\.json|LICENSE|THIRD_PARTY_NOTICES\.md|README\.md|MODEL_DIMENSIONS\.md|cordis\.patch\.yml|lib\/(index|client)\.js|licenses\/[^/]+\.txt)$/)
  67. assert.ok(!/-----BEGIN [A-Z ]*PRIVATE KEY-----|sk-[A-Za-z0-9_-]{30,}/.test(data.toString('utf8')), `Possible credential in ${file}`)
  68. }
  69. for (const [name, version] of Object.entries({ ...manifest.dependencies, ...manifest.peerDependencies })) {
  70. assert.ok(!name.startsWith('@webnovel/') && !/^(workspace:|file:|link:)/.test(version), 'Unpublished runtime dependency')
  71. }
  72. return { ok: true, files: files.size, version: manifest.version }
  73. }
  74. export function checkMetaPackage(filename, expectedVersion, embeddingVersion) {
  75. const files = packageFiles(filename)
  76. const read = name => { assert.ok(files.has(name), `Missing ${name}`); return files.get(name).toString('utf8') }
  77. const manifest = JSON.parse(read('package.json'))
  78. assert.equal(manifest.name, '@linfengqaqtat/dsh-scriptor-full')
  79. assert.equal(manifest.version, expectedVersion)
  80. assert.equal(manifest.license, 'GPL-3.0-only')
  81. checkPublishableManifest(manifest)
  82. assert.deepEqual(manifest.dependencies, {
  83. '@linfengqaqtat/dsh-scriptor': expectedVersion,
  84. 'webnovel-embedding-provider': embeddingVersion,
  85. })
  86. assert.equal(manifest.dsh?.bundle?.patch, './cordis.patch.yml')
  87. assert.equal(read('cordis.patch.yml').replaceAll('\r\n', '\n').trim(),
  88. "- insert:\n - id: webnovel\n name: '@linfengqaqtat/dsh-scriptor'\n - id: webnovel-embeddings\n name: webnovel-embedding-provider")
  89. assert.ok(read('LICENSE').includes('GNU GENERAL PUBLIC LICENSE'))
  90. read('README.md')
  91. for (const file of files.keys()) assert.match(file, /^(package\.json|LICENSE|README\.md|RELEASE_NOTES\.md|cordis\.patch\.yml)$/)
  92. return { ok: true, files: files.size, version: manifest.version }
  93. }