@ctrl/torrent-file 4.2.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -4
- package/dist/src/bencode/decode.js +2 -4
- package/dist/src/bencode/encode.js +1 -3
- package/dist/src/bencode/utils.d.ts +0 -2
- package/dist/src/bencode/utils.js +0 -74
- package/dist/src/torrentFile.d.ts +1 -1
- package/dist/src/torrentFile.js +17 -10
- package/package.json +34 -15
package/README.md
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
# torrent-file [](https://www.npmjs.com/package/@ctrl/torrent-file) [](https://codecov.io/gh/scttcper/torrent-file) [](https://bundlephobia.com/result?p=@ctrl/torrent-file)
|
2
2
|
|
3
|
-
> Parse a torrent file and read encoded data.
|
3
|
+
> Parse a torrent file and read encoded data.
|
4
4
|
|
5
5
|
This project is based on [parse-torrent](https://www.npmjs.com/package/parse-torrent) and [node-bencode](https://github.com/themasch/node-bencode) to parse the data of a torrent file. This library implements its own [bencode](http://www.bittorrent.org/beps/bep_0003.html) encoder and decoder that does not use `Buffer`.
|
6
6
|
|
7
7
|
### Install
|
8
|
+
|
8
9
|
```console
|
9
10
|
npm install @ctrl/torrent-file
|
10
11
|
```
|
@@ -12,9 +13,12 @@ npm install @ctrl/torrent-file
|
|
12
13
|
### API
|
13
14
|
|
14
15
|
##### info
|
16
|
+
|
15
17
|
The content of the metainfo file.
|
18
|
+
|
16
19
|
```ts
|
17
20
|
import fs from 'fs';
|
21
|
+
|
18
22
|
import { info } from '@ctrl/torrent-file';
|
19
23
|
|
20
24
|
const torrentInfo = info(fs.readFileSync('myfile'));
|
@@ -22,9 +26,12 @@ console.log({ torrentInfo });
|
|
22
26
|
```
|
23
27
|
|
24
28
|
##### files
|
29
|
+
|
25
30
|
data about the files described in the torrent file, includes hashes of the pieces
|
31
|
+
|
26
32
|
```ts
|
27
33
|
import fs from 'fs';
|
34
|
+
|
28
35
|
import { files } from '@ctrl/torrent-file';
|
29
36
|
|
30
37
|
const torrentFiles = files(fs.readFileSync('myfile'));
|
@@ -32,18 +39,21 @@ console.log({ torrentFiles });
|
|
32
39
|
```
|
33
40
|
|
34
41
|
##### hash
|
42
|
+
|
35
43
|
sha1 of torrent file info. This hash is commenly used by torrent clients as the ID of the torrent. It is async and sha1 encoding is handled by [crypto-hash](https://github.com/sindresorhus/crypto-hash)
|
44
|
+
|
36
45
|
```ts
|
37
46
|
import fs from 'fs';
|
47
|
+
|
38
48
|
import { hash } from '@ctrl/torrent-file';
|
39
49
|
|
40
50
|
(async () => {
|
41
51
|
const torrentHash = await hash(fs.readFileSync('myfile'));
|
42
52
|
console.log({ torrentHash });
|
43
|
-
})()
|
53
|
+
})();
|
44
54
|
```
|
45
55
|
|
46
|
-
|
47
56
|
### See Also
|
57
|
+
|
48
58
|
[parse-torrent](https://www.npmjs.com/package/parse-torrent) - "@ctrl/torrent-file" torrent parsing based very heavily off this project
|
49
|
-
[node-bencode](https://github.com/themasch/node-bencode) - bencoder built into this project heavily based off this project
|
59
|
+
[node-bencode](https://github.com/themasch/node-bencode) - bencoder built into this project heavily based off this project
|
@@ -1,9 +1,8 @@
|
|
1
1
|
import { stringToUint8Array, uint8ArrayToString } from 'uint8array-extras';
|
2
|
-
import { isValidUTF8 } from './utils.js';
|
3
2
|
const td = new TextDecoder();
|
4
3
|
class Decoder {
|
5
|
-
buf;
|
6
4
|
idx = 0;
|
5
|
+
buf;
|
7
6
|
constructor(buf) {
|
8
7
|
this.buf = buf;
|
9
8
|
}
|
@@ -70,8 +69,7 @@ class Decoder {
|
|
70
69
|
nextBufOrString() {
|
71
70
|
const length = this.readNumber();
|
72
71
|
this.assertByte(':');
|
73
|
-
|
74
|
-
return isValidUTF8(buf) ? td.decode(buf) : buf;
|
72
|
+
return this.readBytes(length);
|
75
73
|
}
|
76
74
|
nextString() {
|
77
75
|
const length = this.readNumber();
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import { concatUint8Arrays, stringToUint8Array } from 'uint8array-extras';
|
2
|
-
import { cmpRawString } from './utils.js';
|
3
2
|
const te = new TextEncoder();
|
4
3
|
const encodeString = (str) => {
|
5
4
|
const lengthBytes = new TextEncoder().encode(str.length.toString());
|
@@ -33,7 +32,7 @@ const encodeNumber = (num) => {
|
|
33
32
|
const encodeDictionary = (obj) => {
|
34
33
|
const results = [];
|
35
34
|
Object.keys(obj)
|
36
|
-
.sort(
|
35
|
+
.sort()
|
37
36
|
.forEach(key => {
|
38
37
|
results.push(encodeString(key));
|
39
38
|
results.push(new Uint8Array(encode(obj[key])));
|
@@ -53,7 +52,6 @@ export const encode = (data) => {
|
|
53
52
|
if (Array.isArray(data)) {
|
54
53
|
return encodeArray(data);
|
55
54
|
}
|
56
|
-
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
|
57
55
|
switch (typeof data) {
|
58
56
|
case 'string': {
|
59
57
|
return encodeString(data);
|
@@ -1,4 +1,3 @@
|
|
1
|
-
/* eslint-disable no-bitwise */
|
2
1
|
// str1 > str2: 1
|
3
2
|
// str1 === str2: 0
|
4
3
|
// str1 < str2: -1
|
@@ -19,76 +18,3 @@ export const cmpRawString = (str1, str2) => {
|
|
19
18
|
}
|
20
19
|
return v1.length < v2.length ? -1 : 1;
|
21
20
|
};
|
22
|
-
export const typedArraysAreEqual = (a, b) => {
|
23
|
-
if (a.byteLength !== b.byteLength) {
|
24
|
-
return false;
|
25
|
-
}
|
26
|
-
return a.every((val, i) => val === b[i]);
|
27
|
-
};
|
28
|
-
// Copied from https://github.com/hcodes/isutf8/blob/master/src/index.ts
|
29
|
-
/*
|
30
|
-
https://tools.ietf.org/html/rfc3629
|
31
|
-
UTF8-char = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4
|
32
|
-
UTF8-1 = %x00-7F
|
33
|
-
UTF8-2 = %xC2-DF UTF8-tail
|
34
|
-
UTF8-3 = %xE0 %xA0-BF UTF8-tail
|
35
|
-
%xE1-EC 2( UTF8-tail )
|
36
|
-
%xED %x80-9F UTF8-tail
|
37
|
-
%xEE-EF 2( UTF8-tail )
|
38
|
-
UTF8-4 = %xF0 %x90-BF 2( UTF8-tail )
|
39
|
-
%xF1-F3 3( UTF8-tail )
|
40
|
-
%xF4 %x80-8F 2( UTF8-tail )
|
41
|
-
UTF8-tail = %x80-BF
|
42
|
-
*/
|
43
|
-
export const isValidUTF8 = (buf) => {
|
44
|
-
let i = 0;
|
45
|
-
const len = buf.length;
|
46
|
-
while (i < len) {
|
47
|
-
// UTF8-1 = %x00-7F
|
48
|
-
if (buf[i] <= 0x7f) {
|
49
|
-
i++;
|
50
|
-
continue;
|
51
|
-
}
|
52
|
-
// UTF8-2 = %xC2-DF UTF8-tail
|
53
|
-
if (buf[i] >= 0xc2 && buf[i] <= 0xdf) {
|
54
|
-
// if(buf[i + 1] >= 0x80 && buf[i + 1] <= 0xBF) {
|
55
|
-
if (buf[i + 1] >> 6 === 2) {
|
56
|
-
i += 2;
|
57
|
-
continue;
|
58
|
-
// biome-ignore lint/style/noUselessElse: false positive
|
59
|
-
}
|
60
|
-
else {
|
61
|
-
return false;
|
62
|
-
}
|
63
|
-
}
|
64
|
-
// UTF8-3 = %xE0 %xA0-BF UTF8-tail
|
65
|
-
// UTF8-3 = %xED %x80-9F UTF8-tail
|
66
|
-
if (((buf[i] === 0xe0 && buf[i + 1] >= 0xa0 && buf[i + 1] <= 0xbf) ||
|
67
|
-
(buf[i] === 0xed && buf[i + 1] >= 0x80 && buf[i + 1] <= 0x9f)) &&
|
68
|
-
buf[i + 2] >> 6 === 2) {
|
69
|
-
i += 3;
|
70
|
-
continue;
|
71
|
-
}
|
72
|
-
// UTF8-3 = %xE1-EC 2( UTF8-tail )
|
73
|
-
// UTF8-3 = %xEE-EF 2( UTF8-tail )
|
74
|
-
if (((buf[i] >= 0xe1 && buf[i] <= 0xec) || (buf[i] >= 0xee && buf[i] <= 0xef)) &&
|
75
|
-
buf[i + 1] >> 6 === 2 &&
|
76
|
-
buf[i + 2] >> 6 === 2) {
|
77
|
-
i += 3;
|
78
|
-
continue;
|
79
|
-
}
|
80
|
-
// UTF8-4 = %xF0 %x90-BF 2( UTF8-tail )
|
81
|
-
// %xF1-F3 3( UTF8-tail )
|
82
|
-
// %xF4 %x80-8F 2( UTF8-tail )
|
83
|
-
if (((buf[i] === 0xf0 && buf[i + 1] >= 0x90 && buf[i + 1] <= 0xbf) ||
|
84
|
-
(buf[i] >= 0xf1 && buf[i] <= 0xf3 && buf[i + 1] >> 6 === 2) ||
|
85
|
-
(buf[i] === 0xf4 && buf[i + 1] >= 0x80 && buf[i + 1] <= 0x8f)) &&
|
86
|
-
buf[i + 2] >> 6 === 2 &&
|
87
|
-
buf[i + 3] >> 6 === 2) {
|
88
|
-
i += 4;
|
89
|
-
continue;
|
90
|
-
}
|
91
|
-
return false;
|
92
|
-
}
|
93
|
-
return true;
|
94
|
-
};
|
@@ -1,6 +1,6 @@
|
|
1
1
|
export declare const sha1: (input: Uint8Array) => string;
|
2
2
|
/**
|
3
|
-
* sha1 of torrent file info. This hash is
|
3
|
+
* sha1 of torrent file info. This hash is commonly used by torrent clients as the ID of the torrent.
|
4
4
|
*/
|
5
5
|
export declare function hash(file: Uint8Array): string;
|
6
6
|
export interface TorrentFileData {
|
package/dist/src/torrentFile.js
CHANGED
@@ -2,6 +2,13 @@ import { createHash } from 'node:crypto';
|
|
2
2
|
import { join, sep } from 'node:path';
|
3
3
|
import { isUint8Array, uint8ArrayToHex, uint8ArrayToString } from 'uint8array-extras';
|
4
4
|
import { decode, encode } from './bencode/index.js';
|
5
|
+
// Helper function to convert Uint8Array to string for display
|
6
|
+
const toString = (value) => {
|
7
|
+
if (value instanceof Uint8Array) {
|
8
|
+
return uint8ArrayToString(value);
|
9
|
+
}
|
10
|
+
return value.toString();
|
11
|
+
};
|
5
12
|
export const sha1 = (input) => {
|
6
13
|
const hash = createHash('sha1');
|
7
14
|
// Update the hash object with the data
|
@@ -9,7 +16,7 @@ export const sha1 = (input) => {
|
|
9
16
|
return hash.digest('hex');
|
10
17
|
};
|
11
18
|
/**
|
12
|
-
* sha1 of torrent file info. This hash is
|
19
|
+
* sha1 of torrent file info. This hash is commonly used by torrent clients as the ID of the torrent.
|
13
20
|
*/
|
14
21
|
export function hash(file) {
|
15
22
|
const torrent = decode(file);
|
@@ -28,9 +35,9 @@ export function files(file) {
|
|
28
35
|
pieces: [],
|
29
36
|
};
|
30
37
|
const files = torrent.info.files || [torrent.info];
|
31
|
-
const name = (torrent.info['name.utf-8'] || torrent.info.name)
|
38
|
+
const name = toString(torrent.info['name.utf-8'] || torrent.info.name);
|
32
39
|
result.files = files.map((file, i) => {
|
33
|
-
const parts = [name, ...(file['path.utf-8'] || file.path || [])].map(p =>
|
40
|
+
const parts = [name, ...(file['path.utf-8'] || file.path || [])].map(p => toString(p));
|
34
41
|
return {
|
35
42
|
path: join(sep, ...parts).slice(1),
|
36
43
|
name: parts[parts.length - 1],
|
@@ -61,7 +68,7 @@ function splitPieces(buf) {
|
|
61
68
|
export function info(file) {
|
62
69
|
const torrent = decode(file);
|
63
70
|
const result = {
|
64
|
-
name: (torrent.info['name.utf-8'] || torrent.info.name)
|
71
|
+
name: toString(torrent.info['name.utf-8'] || torrent.info.name),
|
65
72
|
announce: [],
|
66
73
|
urlList: [],
|
67
74
|
};
|
@@ -72,10 +79,10 @@ export function info(file) {
|
|
72
79
|
result.created = new Date(torrent['creation date'] * 1000);
|
73
80
|
}
|
74
81
|
if (torrent['created by']) {
|
75
|
-
result.createdBy = torrent['created by']
|
82
|
+
result.createdBy = toString(torrent['created by']);
|
76
83
|
}
|
77
|
-
if (
|
78
|
-
result.comment =
|
84
|
+
if (torrent.comment) {
|
85
|
+
result.comment = toString(torrent.comment);
|
79
86
|
}
|
80
87
|
// announce and announce-list will be missing if metadata fetched via ut_metadata
|
81
88
|
if (Array.isArray(torrent['announce-list']) &&
|
@@ -83,12 +90,12 @@ export function info(file) {
|
|
83
90
|
torrent['announce-list'].length > 0) {
|
84
91
|
torrent['announce-list'].forEach((urls) => {
|
85
92
|
urls.forEach((url) => {
|
86
|
-
result.announce.push(
|
93
|
+
result.announce.push(toString(url));
|
87
94
|
});
|
88
95
|
});
|
89
96
|
}
|
90
97
|
else if (torrent.announce) {
|
91
|
-
result.announce.push(torrent.announce
|
98
|
+
result.announce.push(toString(torrent.announce));
|
92
99
|
}
|
93
100
|
if (result.announce.length) {
|
94
101
|
result.announce = Array.from(new Set(result.announce));
|
@@ -98,7 +105,7 @@ export function info(file) {
|
|
98
105
|
// some clients set url-list to empty string
|
99
106
|
torrent['url-list'] = torrent['url-list'].length > 0 ? [torrent['url-list']] : [];
|
100
107
|
}
|
101
|
-
result.urlList = (torrent['url-list'] || []).map((url) =>
|
108
|
+
result.urlList = (torrent['url-list'] || []).map((url) => toString(url));
|
102
109
|
if (result.urlList.length) {
|
103
110
|
result.urlList = Array.from(new Set(result.urlList));
|
104
111
|
}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@ctrl/torrent-file",
|
3
|
-
"version": "4.
|
3
|
+
"version": "4.4.0",
|
4
4
|
"description": "Parse a torrent file (name, hash, files, pieces)",
|
5
5
|
"author": "Scott Cooper <scttcper@gmail.com>",
|
6
6
|
"license": "MIT",
|
@@ -17,12 +17,8 @@
|
|
17
17
|
"sideEffects": false,
|
18
18
|
"keywords": [],
|
19
19
|
"scripts": {
|
20
|
-
"lint": "
|
21
|
-
"lint:
|
22
|
-
"lint:eslint": "eslint .",
|
23
|
-
"lint:fix": "pnpm run '/^(lint:biome|lint:eslint):fix$/'",
|
24
|
-
"lint:eslint:fix": "eslint . --fix",
|
25
|
-
"lint:biome:fix": "biome check . --apply",
|
20
|
+
"lint": "oxlint . && prettier --check . --experimental-cli",
|
21
|
+
"lint:fix": "oxlint . --fix && prettier --write . --log-level=error --experimental-cli",
|
26
22
|
"prepare": "npm run build",
|
27
23
|
"build": "tsc",
|
28
24
|
"test": "vitest run",
|
@@ -30,18 +26,41 @@
|
|
30
26
|
"test:ci": "vitest run --coverage --reporter=default --reporter=junit --outputFile=./junit.xml"
|
31
27
|
},
|
32
28
|
"dependencies": {
|
33
|
-
"uint8array-extras": "^1.
|
29
|
+
"uint8array-extras": "^1.5.0"
|
34
30
|
},
|
35
31
|
"devDependencies": {
|
36
|
-
"@
|
37
|
-
"@
|
38
|
-
"@
|
39
|
-
"@types/node": "
|
40
|
-
"@vitest/coverage-v8": "3.
|
32
|
+
"@ctrl/oxlint-config": "1.2.7",
|
33
|
+
"@sindresorhus/tsconfig": "8.0.1",
|
34
|
+
"@trivago/prettier-plugin-sort-imports": "5.2.2",
|
35
|
+
"@types/node": "24.6.2",
|
36
|
+
"@vitest/coverage-v8": "3.2.4",
|
37
|
+
"oxlint": "1.19.0",
|
41
38
|
"parse-torrent": "11.0.18",
|
42
|
-
"
|
43
|
-
"
|
39
|
+
"prettier": "3.6.2",
|
40
|
+
"typescript": "5.9.3",
|
41
|
+
"vitest": "3.2.4"
|
44
42
|
},
|
43
|
+
"prettier": {
|
44
|
+
"singleQuote": true,
|
45
|
+
"trailingComma": "all",
|
46
|
+
"arrowParens": "avoid",
|
47
|
+
"semi": true,
|
48
|
+
"printWidth": 100,
|
49
|
+
"plugins": [
|
50
|
+
"@trivago/prettier-plugin-sort-imports"
|
51
|
+
],
|
52
|
+
"importOrder": [
|
53
|
+
"^node:.*$",
|
54
|
+
"<THIRD_PARTY_MODULES>",
|
55
|
+
"^(@ctrl)(/.*|$)",
|
56
|
+
"^\\.\\./(?!.*\\.css$)",
|
57
|
+
"^\\./(?!.*\\.css$)(?=.*/)",
|
58
|
+
"^\\./(?!.*\\.css$)(?!.*/)"
|
59
|
+
],
|
60
|
+
"importOrderSeparation": true,
|
61
|
+
"importOrderSortSpecifiers": false
|
62
|
+
},
|
63
|
+
"packageManager": "pnpm@10.18.0",
|
45
64
|
"publishConfig": {
|
46
65
|
"access": "public",
|
47
66
|
"provenance": true
|