code wiki / (root) / nx_torrent_mkinfo.nx

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}