1313//! kept% — output bytes / input bytes (less dead weight => lower)
1414//! opaque% — mean fraction of identifiers that are hex (`_0x..`) or 1-2 chars
1515//! (more meaningful names => lower)
16+ //!
17+ //! Publishing flags drive the Hugging Face dataset page:
18+ //! --json <path> write per-sample rows (incl. input/output code) as JSONL,
19+ //! which the HF Dataset Viewer renders as a browsable table
20+ //! --markdown print the aggregate scoreboard as a GitHub table (for the
21+ //! dataset card / README), instead of the fixed-width table
1622use devirt_core:: deobfuscate;
1723use std:: collections:: BTreeMap ;
18- use std:: fs;
24+ use std:: fs:: { self , File } ;
25+ use std:: io:: { BufWriter , Write } ;
1926use std:: path:: { Path , PathBuf } ;
2027
2128#[ derive( Default ) ]
@@ -30,9 +37,12 @@ struct Agg {
3037fn main ( ) {
3138 let args: Vec < String > = std:: env:: args ( ) . skip ( 1 ) . collect ( ) ;
3239 let show_files = args. iter ( ) . any ( |a| a == "--files" ) ;
40+ let markdown = args. iter ( ) . any ( |a| a == "--markdown" ) ;
41+ let json_path = flag_value ( & args, "--json" ) ;
3342 let root = args
3443 . iter ( )
3544 . find ( |a| !a. starts_with ( "--" ) )
45+ . filter ( |a| Some ( a. as_str ( ) ) != json_path. as_deref ( ) )
3646 . map ( PathBuf :: from)
3747 . unwrap_or_else ( || PathBuf :: from ( "samples" ) ) ;
3848
@@ -48,6 +58,12 @@ fn main() {
4858 std:: process:: exit ( 1 ) ;
4959 }
5060
61+ // Optional per-sample JSONL for the HF Dataset Viewer.
62+ let mut json = json_path. as_ref ( ) . map ( |p| {
63+ let w = BufWriter :: new ( File :: create ( p) . expect ( "create --json file" ) ) ;
64+ w
65+ } ) ;
66+
5167 let mut groups: BTreeMap < String , Agg > = BTreeMap :: new ( ) ;
5268 let mut total = Agg :: default ( ) ;
5369 if show_files {
@@ -60,8 +76,10 @@ fn main() {
6076 let opaque_in = opaque_ratio ( & src) ;
6177 let r = deobfuscate ( & src, & name) ;
6278 let opaque_out = opaque_ratio ( & r. code ) ;
79+ let rel_path = rel ( & root, p) ;
80+ let source = group_of ( & root, p) ;
6381
64- let g = groups. entry ( group_of ( & root , p ) ) . or_default ( ) ;
82+ let g = groups. entry ( source . clone ( ) ) . or_default ( ) ;
6583 g. files += 1 ;
6684 g. in_bytes += src. len ( ) ;
6785 g. out_bytes += r. code . len ( ) ;
@@ -73,10 +91,14 @@ fn main() {
7391 total. opaque_in_sum += opaque_in;
7492 total. opaque_out_sum += opaque_out;
7593
94+ if let Some ( w) = json. as_mut ( ) {
95+ write_json_row ( w, & rel_path, & source, & src, & r. code , opaque_in, opaque_out) ;
96+ }
97+
7698 if show_files {
7799 println ! (
78100 "{:<52} {:>9} {:>9} {:>5}% {:>6.1}%→{:>5.1}%" ,
79- trunc( & rel ( & root , p ) , 52 ) ,
101+ trunc( & rel_path , 52 ) ,
80102 src. len( ) ,
81103 r. code. len( ) ,
82104 pct( r. code. len( ) , src. len( ) ) ,
@@ -86,17 +108,30 @@ fn main() {
86108 }
87109 }
88110
111+ if let Some ( w) = json. as_mut ( ) {
112+ w. flush ( ) . expect ( "flush --json file" ) ;
113+ eprintln ! ( "wrote {} rows to {}" , total. files, json_path. unwrap( ) ) ;
114+ }
115+
89116 if show_files {
90117 println ! ( ) ;
91118 }
92- println ! (
93- "{:<24} {:>7} {:>11} {:>11} {:>6} {:>16}" ,
94- "source" , "files" , "in" , "out" , "kept%" , "opaque% in→out"
95- ) ;
96- for ( name, a) in & groups {
97- print_row ( name, a) ;
119+ if markdown {
120+ print_markdown ( & groups, & total) ;
121+ } else {
122+ println ! (
123+ "{:<24} {:>7} {:>11} {:>11} {:>6} {:>16}" ,
124+ "source" , "files" , "in" , "out" , "kept%" , "opaque% in→out"
125+ ) ;
126+ for ( name, a) in & groups {
127+ print_row ( name, a) ;
128+ }
129+ print_row ( "TOTAL" , & total) ;
98130 }
99- print_row ( "TOTAL" , & total) ;
131+ }
132+
133+ fn flag_value ( args : & [ String ] , flag : & str ) -> Option < String > {
134+ args. iter ( ) . position ( |a| a == flag) . and_then ( |i| args. get ( i + 1 ) ) . cloned ( )
100135}
101136
102137fn print_row ( name : & str , a : & Agg ) {
@@ -113,6 +148,66 @@ fn print_row(name: &str, a: &Agg) {
113148 ) ;
114149}
115150
151+ /// GitHub-flavored markdown table for the dataset card.
152+ fn print_markdown ( groups : & BTreeMap < String , Agg > , total : & Agg ) {
153+ println ! ( "| source | files | in bytes | out bytes | kept% | opaque% in→out |" ) ;
154+ println ! ( "|---|--:|--:|--:|--:|--:|" ) ;
155+ let row = |name : & str , a : & Agg | {
156+ let n = a. files . max ( 1 ) as f64 ;
157+ println ! (
158+ "| `{}` | {} | {} | {} | {}% | {:.1}% → {:.1}% |" ,
159+ name, a. files, a. in_bytes, a. out_bytes, pct( a. out_bytes, a. in_bytes) ,
160+ a. opaque_in_sum / n, a. opaque_out_sum / n,
161+ ) ;
162+ } ;
163+ for ( name, a) in groups {
164+ row ( name, a) ;
165+ }
166+ row ( "TOTAL" , total) ;
167+ }
168+
169+ /// One JSONL row per sample. Hand-rolled JSON (no serde dep) — only strings need
170+ /// escaping, and `js_escape` covers the JSON string grammar.
171+ fn write_json_row (
172+ w : & mut impl Write ,
173+ path : & str ,
174+ source : & str ,
175+ input : & str ,
176+ output : & str ,
177+ opaque_in : f64 ,
178+ opaque_out : f64 ,
179+ ) {
180+ let _ = writeln ! (
181+ w,
182+ "{{\" path\" :\" {}\" ,\" source\" :\" {}\" ,\" in_bytes\" :{},\" out_bytes\" :{},\" kept_pct\" :{},\" opaque_in\" :{:.1},\" opaque_out\" :{:.1},\" input\" :\" {}\" ,\" output\" :\" {}\" }}" ,
183+ json_escape( path) ,
184+ json_escape( source) ,
185+ input. len( ) ,
186+ output. len( ) ,
187+ pct( output. len( ) , input. len( ) ) ,
188+ opaque_in,
189+ opaque_out,
190+ json_escape( input) ,
191+ json_escape( output) ,
192+ ) ;
193+ }
194+
195+ fn json_escape ( s : & str ) -> String {
196+ let mut out = String :: with_capacity ( s. len ( ) + 16 ) ;
197+ for c in s. chars ( ) {
198+ match c {
199+ '"' => out. push_str ( "\\ \" " ) ,
200+ '\\' => out. push_str ( "\\ \\ " ) ,
201+ '\n' => out. push_str ( "\\ n" ) ,
202+ '\r' => out. push_str ( "\\ r" ) ,
203+ '\t' => out. push_str ( "\\ t" ) ,
204+ c if ( c as u32 ) < 0x20 => out. push_str ( & format ! ( "\\ u{:04x}" , c as u32 ) ) ,
205+ c => out. push ( c) ,
206+ }
207+ }
208+ out
209+ }
210+
116211fn collect ( dir : & Path , out : & mut Vec < PathBuf > ) {
117212 let Ok ( rd) = fs:: read_dir ( dir) else { return } ;
118213 for e in rd. flatten ( ) {
0 commit comments