@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 CHANGED
@@ -1,10 +1,11 @@
1
1
  # torrent-file [![npm](https://badgen.net/npm/v/@ctrl/torrent-file)](https://www.npmjs.com/package/@ctrl/torrent-file) [![coverage](https://badgen.net/codecov/c/github/scttcper/torrent-file)](https://codecov.io/gh/scttcper/torrent-file) [![bundlesize](https://badgen.net/bundlephobia/min/@ctrl/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
- const buf = this.readBytes(length);
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(cmpRawString)
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,3 +1 @@
1
1
  export declare const cmpRawString: (str1: string, str2: string) => number;
2
- export declare const typedArraysAreEqual: <T extends Uint8Array>(a: T, b: T) => boolean;
3
- export declare const isValidUTF8: (buf: Uint8Array) => boolean;
@@ -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 commenly used by torrent clients as the ID of the torrent.
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 {
@@ -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 commenly used by torrent clients as the ID of the torrent.
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).toString();
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 => p.toString());
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).toString(),
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'].toString();
82
+ result.createdBy = toString(torrent['created by']);
76
83
  }
77
- if (isUint8Array(torrent.comment)) {
78
- result.comment = uint8ArrayToString(torrent.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(url.toString());
93
+ result.announce.push(toString(url));
87
94
  });
88
95
  });
89
96
  }
90
97
  else if (torrent.announce) {
91
- result.announce.push(torrent.announce.toString());
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) => url.toString());
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.2.0",
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": "pnpm run '/^(lint:biome|lint:eslint)$/'",
21
- "lint:biome": "biome check .",
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.4.0"
29
+ "uint8array-extras": "^1.5.0"
34
30
  },
35
31
  "devDependencies": {
36
- "@biomejs/biome": "1.9.4",
37
- "@ctrl/eslint-config-biome": "4.3.1",
38
- "@sindresorhus/tsconfig": "7.0.0",
39
- "@types/node": "22.10.7",
40
- "@vitest/coverage-v8": "3.0.2",
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
- "typescript": "5.7.3",
43
- "vitest": "3.0.2"
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