nx_torrent_mkinfo.nx source
↩ module page · 164 lines · 9857 B
1// nx_torrent_mkinfo.nx -- BUILD a BEP-3 v1 single-file torrent info-dict from a REAL file.
2//
3// The seeder (nx_torrent_seed) can serve any file, but a REAL leecher can only find + verify it if it has
4// the file's v1 info_hash and per-piece SHA-1s -- i.e. a .torrent/magnet. nx_metainfo only PARSES a
5// .torrent; this is the inverse (the "originate/create a torrent" half): read a file, SHA-1 each piece,
6// bencode the info-dict (length/name/piece length/pieces, keys sorted per BEP-3), SHA-1 it -> info_hash,
7// and emit a magnet + a seed_index.conf row. This is what lets the hub SHARE content that any mainstream
8// BitTorrent client (qBittorrent/Transmission/libtorrent) can pull + integrity-check.
9//
10// nx_torrent_mkinfo <file> <name> [piece_length] -> info_hash / magnet / seed row (default plen=262144)
11// nx_torrent_mkinfo -> no-arg = the GATE (round-trips through nx_metainfo)
12//
13// NEVER-BRICK: reads a file + prints; writes no persistent state. license_tier: ORIGINAL
14// module: nishi-core.torrent.mkinfo depends: nishi-core.torrent.metainfo (pulls bencode+sha1+syscalls)
15import "nx_metainfo.nx"
16const MK_MAGIC_4096: i64 = 4096
17
18const MK_PLEN_DEFAULT: i64 = 262144 // 256 KiB (standard)
19
20func mk_w(fd: i64, s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(fd,s,n); return 0 }
21func mk_wn(fd: i64, v: i64) -> i64 {
22 let t: *u8=sys_mmap(28); var m: i64=v; if m<0 {m=0-m; sys_write(fd,"-" as *u8,1)}
23 var k: i64=0; if m==0 {t[0]=48 as u8;k=1}; while m>0 {t[k]=(48+(m%10)) as u8; m=m/10; k=k+1}
24 let b: *u8=sys_mmap(28); var i: i64=0; while i<k {b[i]=t[k-1-i]; i=i+1}; sys_write(fd,b,k); return 0
25}
26func mk_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
27func mk_atoi(s: *u8) -> i64 { var v: i64=0; var i: i64=0; while s[i]!=(0 as u8){ let c: i64=s[i] as i64; if c<48 {return v} if c>57 {return v} v=v*10+(c-48); i=i+1 } return v }
28func mk_read_n(fd: i64, buf: *u8, n: i64) -> i64 { var got: i64=0; while got<n { let r: i64=sys_read(fd, buf+got, n-got); if r<=0 { return got } got=got+r } return got }
29
30// ---- bencode writer helpers (append into dst at off, return new off) ----
31func mk_cat(dst: *u8, off: i64, s: *u8) -> i64 { var o: i64=off; var i: i64=0; while s[i]!=(0 as u8){dst[o]=s[i];o=o+1;i=i+1} return o }
32func mk_catbytes(dst: *u8, off: i64, src: *u8, n: i64) -> i64 { var o: i64=off; var i: i64=0; while i<n {dst[o]=src[i];o=o+1;i=i+1} return o }
33func mk_catn(dst: *u8, off: i64, v: i64) -> i64 {
34 if v==0 { dst[off]=48 as u8; return off+1 }
35 let t: *u8=sys_mmap(28); var m: i64=v; var k: i64=0; while m>0 {t[k]=(48+(m%10)) as u8; m=m/10; k=k+1}
36 var o: i64=off; var i: i64=k-1; while i>=0 {dst[o]=t[i];o=o+1;i=i-1} return o
37}
38// bencoded string: <len>:<bytes>
39func mk_bstr(dst: *u8, off: i64, src: *u8, n: i64) -> i64 { var o: i64=mk_catn(dst,off,n); dst[o]=58 as u8; o=o+1; o=mk_catbytes(dst,o,src,n); return o }
40// bencoded int: i<v>e
41func mk_bint(dst: *u8, off: i64, v: i64) -> i64 { dst[off]=105 as u8; var o: i64=mk_catn(dst,off+1,v); dst[o]=101 as u8; return o+1 }
42
43func mk_hex(dst: *u8, off: i64, bytes: *u8, n: i64) -> i64 {
44 let hx: *u8 = "0123456789abcdef" as *u8
45 var o: i64=off; var i: i64=0
46 while i<n { let b: i64=bytes[i] as i64; dst[o]=hx[(b>>4)&0xf]; dst[o+1]=hx[b&0xf]; o=o+2; i=i+1 }
47 return o
48}
49
50// st_size lives at offset 48 of the x86_64 struct stat (st_mtime is at 88, used elsewhere).
51func mk_filesize(path: *u8) -> i64 {
52 let st: *u8 = sys_mmap(160)
53 if sys_fstatat(path, st) == 0 { let sp: *i64 = (((st as i64)+48)) as *i64; return sp[0] }
54 return 0-1
55}
56
57// Build the info-dict for `path` into `info` (returns info_len); write per-piece sha1 into `pieces`
58// (npc*20 bytes); write info_hash (sha1 of the info-dict) into ih_out; return info_len, or -1 on error.
59// Keys sorted per BEP-3: "length" < "name" < "piece length" < "pieces".
60func mk_build_info(path: *u8, name: *u8, plen: i64, total: i64, npc: i64, info: *u8, pieces: *u8, ih_out: *u8) -> i64 {
61 let fd: i64 = sys_openat_rd(path); if fd<0 { return 0-1 }
62 let pbuf: *u8 = sys_mmap(plen + 64)
63 var idx: i64 = 0
64 while idx<npc {
65 var want: i64 = plen; if idx==npc-1 { want = total - idx*plen }
66 let got: i64 = mk_read_n(fd, pbuf, want)
67 if got != want { sys_close(fd); return 0-2 }
68 let dst: *u8 = pieces + idx*20
69 sha1(pbuf, want, dst)
70 idx = idx+1
71 }
72 sys_close(fd)
73 let namelen: i64 = mk_slen(name)
74 var o: i64 = 0
75 o = mk_cat(info, o, "d" as *u8)
76 o = mk_bstr(info, o, "length" as *u8, 6); o = mk_bint(info, o, total)
77 o = mk_bstr(info, o, "name" as *u8, 4); o = mk_bstr(info, o, name, namelen)
78 o = mk_bstr(info, o, "piece length" as *u8, 12); o = mk_bint(info, o, plen)
79 o = mk_bstr(info, o, "pieces" as *u8, 6); o = mk_bstr(info, o, pieces, npc*20)
80 o = mk_cat(info, o, "e" as *u8)
81 sha1(info, o, ih_out)
82 return o
83}
84
85// ---- emit mode: real file -> info_hash / magnet / seed row ----
86func mk_emit(path: *u8, name: *u8, plen: i64) -> i64 {
87 let total: i64 = mk_filesize(path)
88 if total < 0 { mk_w(1, "mkinfo: cannot stat file: " as *u8); mk_w(1, path); mk_w(1, "\n" as *u8); sys_exit(1); return 1 }
89 let npc: i64 = (total + plen - 1) / plen
90 let info: *u8 = sys_mmap(npc*20 + MK_MAGIC_4096)
91 let pieces: *u8 = sys_mmap(npc*20 + 64)
92 let ih: *u8 = sys_mmap(20)
93 let ilen: i64 = mk_build_info(path, name, plen, total, npc, info, pieces, ih)
94 if ilen < 0 { mk_w(1, "mkinfo: read error building info-dict\n" as *u8); sys_exit(1); return 1 }
95 let hexih: *u8 = sys_mmap(48); mk_hex(hexih, 0, ih, 20); hexih[40]=0 as u8
96 mk_w(1, "info_hash=" as *u8); mk_w(1, hexih); mk_w(1, "\n" as *u8)
97 mk_w(1, "name=" as *u8); mk_w(1, name); mk_w(1, "\n" as *u8)
98 mk_w(1, "piece_length=" as *u8); mk_wn(1, plen); mk_w(1, "\n" as *u8)
99 mk_w(1, "total=" as *u8); mk_wn(1, total); mk_w(1, "\n" as *u8)
100 mk_w(1, "pieces=" as *u8); mk_wn(1, npc); mk_w(1, "\n" as *u8)
101 mk_w(1, "magnet=magnet:?xt=urn:btih:" as *u8); mk_w(1, hexih); mk_w(1, "&dn=" as *u8); mk_w(1, name); mk_w(1, "\n" as *u8)
102 // a directly-appendable seed_index.conf row: <name>\t<ih_hex>\t<plen>\t<total>\t<src>
103 mk_w(1, "seedrow=" as *u8); mk_w(1, name); mk_w(1, "\t" as *u8); mk_w(1, hexih); mk_w(1, "\t" as *u8); mk_wn(1, plen); mk_w(1, "\t" as *u8); mk_wn(1, total); mk_w(1, "\t" as *u8); mk_w(1, path); mk_w(1, "\n" as *u8)
104 sys_exit(0); return 0
105}
106
107// ---- gate: build info for a known file, wrap it in a full .torrent, and RE-PARSE via nx_metainfo.
108// A byte-correct bencode is proven when the parser recovers the SAME info_hash / piece_length / total. ----
109func mk_gate() -> i64 {
110 let path: *u8 = "/tmp/_nx_mkinfo_test.bin" as *u8
111 let name: *u8 = "mkinfo_test.bin" as *u8
112 let plen: i64 = 64
113 let total: i64 = 200 // 4 pieces: 64,64,64,8
114 // write a deterministic file
115 let fb: *u8 = sys_mmap(total+16); var i: i64=0; while i<total { fb[i]=((i*73+11)&0xff) as u8; i=i+1 }
116 let wf: i64 = sys_openat_wr(path, 0x1a4); if wf<0 { mk_w(1,"MKINFO-GATE file_write=FAIL verdict=RED\n" as *u8); sys_exit(1); return 1 }
117 sys_write(wf, fb, total); sys_close(wf)
118 let npc: i64 = (total+plen-1)/plen
119 let info: *u8 = sys_mmap(npc*20+MK_MAGIC_4096); let pieces: *u8 = sys_mmap(npc*20+64); let ih: *u8 = sys_mmap(20)
120 let ilen: i64 = mk_build_info(path, name, plen, total, npc, info, pieces, ih)
121 var built_ok: i64=0; if ilen>0 { built_ok=1 }
122 // wrap: d 8:announce <url> 4:info <info bytes> e
123 let ann: *u8 = "udp://tracker.local:6969" as *u8; let annlen: i64 = mk_slen(ann)
124 let tor: *u8 = sys_mmap(ilen+MK_MAGIC_4096)
125 var o: i64=0
126 o = mk_cat(tor, o, "d" as *u8)
127 o = mk_bstr(tor, o, "announce" as *u8, 8); o = mk_bstr(tor, o, ann, annlen)
128 o = mk_bstr(tor, o, "info" as *u8, 4); o = mk_catbytes(tor, o, info, ilen)
129 o = mk_cat(tor, o, "e" as *u8)
130 // parse it back
131 let mi: *NxMetainfo = sys_mmap(NX_METAINFO_BYTES) as *NxMetainfo
132 let ih2: *u8 = sys_mmap(20)
133 let pr: i64 = nx_metainfo_parse(tor, o, mi, ih2)
134 var parse_ok: i64=0; if pr==NX_MI_OK { parse_ok=1 }
135 var ih_eq: i64=1; i=0; while i<20 { if ih[i]!=ih2[i] { ih_eq=0; i=20 } else { i=i+1 } }
136 var plen_eq: i64=0; if mi.piece_length==plen { plen_eq=1 }
137 var total_eq: i64=0; if mi.total_length==total { total_eq=1 }
138 var pieces_len_ok: i64=0
139 // announce round-trips too (sanity the wrapper)
140 var ann_ok: i64=0; if mi.announce_len==annlen { ann_ok=1 }
141 if built_ok==1 { pieces_len_ok=1 } // mk_build_info succeeded => pieces buffer is npc*20 by construction
142 let hexih: *u8 = sys_mmap(48); mk_hex(hexih,0,ih,20); hexih[40]=0 as u8
143 mk_w(1, "MKINFO-GATE authored=organ built=" as *u8); mk_wn(1, built_ok)
144 mk_w(1, " reparse=" as *u8); mk_wn(1, parse_ok)
145 mk_w(1, " info_hash_eq=" as *u8); mk_wn(1, ih_eq)
146 mk_w(1, " piece_length_eq=" as *u8); mk_wn(1, plen_eq)
147 mk_w(1, " total_eq=" as *u8); mk_wn(1, total_eq)
148 mk_w(1, " announce_eq=" as *u8); mk_wn(1, ann_ok)
149 mk_w(1, " info_hash=" as *u8); mk_w(1, hexih)
150 var allok: i64=0
151 if built_ok==1 { if parse_ok==1 { if ih_eq==1 { if plen_eq==1 { if total_eq==1 { if ann_ok==1 { allok=1 } } } } } }
152 if allok==1 { mk_w(1, " verdict=GREEN\n" as *u8); sys_exit(0); return 0 }
153 mk_w(1, " verdict=RED\n" as *u8); sys_exit(1); return 1
154}
155
156func main(argc: i64, argv: *i64) -> i64 {
157 if argc < 2 { return mk_gate() }
158 if argc < 3 { mk_w(1, "usage: nx_torrent_mkinfo <file> <name> [piece_length]\n" as *u8); sys_exit(2); return 2 }
159 let path: *u8 = argv[1] as *u8
160 let name: *u8 = argv[2] as *u8
161 var plen: i64 = MK_PLEN_DEFAULT
162 if argc >= 4 { let p: i64 = mk_atoi(argv[3] as *u8); if p>0 { plen=p } }
163 return mk_emit(path, name, plen)
164}