From 92c815026ca8fd465a66fbc34bff23fec6a00b17 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Wed, 8 Jul 2026 13:40:24 -0700 Subject: [PATCH] fix(file): classify scripts before magic detection --- docs-internal/registry-parity-worklist.md | 14 ++ software/file/bin/file | Bin 119532 -> 119644 bytes .../file/native/crates/file-cmd/src/lib.rs | 64 ++++---- software/file/test/file.test.ts | 152 ++++++++++++++++++ 4 files changed, 198 insertions(+), 32 deletions(-) create mode 100644 software/file/test/file.test.ts diff --git a/docs-internal/registry-parity-worklist.md b/docs-internal/registry-parity-worklist.md index a2385e2101..9ec76a2f73 100644 --- a/docs-internal/registry-parity-worklist.md +++ b/docs-internal/registry-parity-worklist.md @@ -636,6 +636,20 @@ real e2e tests that prove Linux-parity behavior — not smoke tests. Biome is not applicable for this package test path; it reported the file is ignored by config in `2026-07-08T13-35-04-0700-item12-diffutils-biome-check.log`. + - **file — DONE.** Added package-local VM e2e coverage for the staged `file` + command and fixed shebang script classification to run before generic magic + detection, producing Linux-like script descriptions instead of raw + `text/x-shellscript`. The suite proves text, JSON, script, PNG, empty-file, + directory, brief, MIME, stdin, and missing-input behavior through the + packaged WASM command. Proof: + `2026-07-08T13-39-36-0700-item12-file-toolchain-cmd-build.log`; + `2026-07-08T13-40-33-0700-item12-file-package-build-after-shebang-fix.log`; + `2026-07-08T13-41-05-0700-item12-file-package-e2e-final-after-install.log`; + `2026-07-08T13-41-05-0700-item12-file-check-types-final-after-install.log`; + `2026-07-08T13-40-47-0700-item12-file-cargo-fmt-check-final.log`. + Biome is not applicable for this package test path; it reported the file is + ignored by config in + `2026-07-08T13-41-05-0700-item12-file-biome-check-final-after-install.log`. - **jq — DONE.** Added package-local VM e2e coverage for the staged `jq` command and fixed the jaq-backed CLI wrapper to accept Linux-style file operands instead of only stdin. The suite now proves version output, diff --git a/software/file/bin/file b/software/file/bin/file index 1e32fe60640045a928831e23813298c91080116c..b134518d9d2771438d8b4e801103c6fa5e948e53 100644 GIT binary patch delta 19585 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 Og3uys>1q=TMfg7#{!6(4 delta 19577 zcmb7s349dA68BW^W^-(g;R?A(b~g|LAp{5zK#)uj5CyqBK;^zDF%MO&Bv zK_h}j1ObC4awdohf{F-&iV9DB@)QMms6kP_fA!2JpuX?>48Pv)s_L_@uI_0)`9kQq z^`To%i_))zAR!#`Ylpw{Q$mWdb;k6&@4=z7rj46NBl4;BT+wpQTnelgFN$?ygYfSa zna#6WG;Q57r|lk5A?_JGWCfX2h<_{PBhC!RR(e&IJKM(`rX%zb9i?ORF`b}Ko(jnc z{6baqswfrP#B1VBu}{1u-WKnO{bGmMDRzm?qWMSSs5mA*7RSXw@t*iVd?<1%#cpv# zL{db=*YiS-3rR!)k!$3y+N-4tt*g^28q`*3M#bvA)(-g9QrD`E3`E&lNUp~roSwJZSCh88vi&A8mDgrGF_y59NnC`AgBBFwQ@+< zOe!ut5!OSXrO#bR_sbpb@pOM_dSo3MR*vOoYMLR&MGJBWx5w>qpyi4GRBHMo!Y9zN z&*?!H<`c$QU=5-7?HUyl8SXY-_zB>TnsM$DDr52ncV>tVnr1rEQFAErMli=upi zWgq}|LHLdID-jq@)pVey+b9H01oWlC*T&#_O&nT32%`u^{V}0Xo;EoS&7erKYGcJd zG6LPAm<8l_gRAJA+qM-2WUS{|lE-DxWM5RGx1kXuJ4J`pf7C()lV(I2ng@jj#^tl} z-sq;HxJnj8PwG~6mRs`r$hd;;f{bx7V>Bbo?|~38g6=o2EXMR67mSGXlC?Gl!t^KP zjWKy(_1u`TG*NyR(~_Q)4fLFh;;KqsTaExb^*5l1%xT46viR%JpTvS7=5iU(Z?5yo z8x=Os^ruB+EjMN)Kh+xrcwz>#&bWk99y){sxRhU^%4bpLnN$hZwliE?Q;g;!IEuNR zAisVq*Evbv7y&B~xyA)hnXwMlEtj0#RKHwQ>pVq1>NUAOTMCZ>yf+L>aP|PVs#eM6 zwff&vWU0ANY|8U2@emvhdU9UYZ#d?P~Ceq4d*Co^X{)K62o!1|yB6C=H&=ILsKI$kgf@ z5#6G|HP$4h>rM#nz_(yTxZW32e&feV%^MQwbQ>`NFIeso#u27QDC!1_8iFoCI9b!I zDA-I9zOHHU*mW(=*R;5L!5&DpsH$mk!6JmX1Bx>5JB%KsM*r7!O`c#vq#_)>jFBy zuR{!CG#mV!L2FS(Ru7(ytO8a7*$OS7>m9K)sp-|iyBc)HI^~aQkpA2s1=9T z5Lec;yl4@vmc~~KQqz9k?jXvkz>N&LF>MPh`#wW zF0fJR3p;f1K9$9c@j-$o&OlL5 zk{#m{!rp<=U~ivXIyn9`WtL+n)W^e&9i9kde-Mbk*hzlaFnJ<+B!Y56QRMi zI9+y3TAZ|4`N5tr^8EaL8hZ+idNaZpp%=(6lMYdV+}L0Ot(J+&zT z1)?0dJ>J!gLzs|WB;QPK9CniD=Ft1fi^=0_uU5UMV*#4s@uSbxa#D&~?TI$N{84_F;z_r>wqbc^Jq?7*m^=(V4ek-dIG#m_zDC}i zn$cv98cxTev52}@Qwh6`5PhNG@B>3_vi!?J`ATX6n0+91Sni>Vs1mJu{*;S4dWN{r z!3E(Zkqh_gLd(~=Wi)!imZ}i@?5?H1;h?}pUbItn@-r9X&7}^{Q__sX$n@DzJ8XT%w_G8VeG2%+h#wh&@G}ZQ0l@z<$i{1dfBdI1 zL^E8_lJjZFv`|JjP7YhZ3>Am~S~vE%3IIT%?rxk(on%4dyujz>&=o5@QM0gzsg436 zl@O(t2w71QW#On+hTYqBWQY+;Dtn}R zBd?uY*8i{<<29h)CU0E8gYM}4qwp0EI~hFw|93-pZ= z3WYMw#wLAYm3-!WgD7s|Aq3EFqhggD-lSRJ6fl_@BHeM(4&fjW7^4Dr)=mDW0(7pU z&IJNhRK#w__6_}pra#IPMa1o~vN#N@Y0C4>I z0H9{+C3P4u2vIDSxQy51zno+>x^MNVIh9IK0W*lCKQO={zBCF09VH6&Kwui}S8Fvaq zcZV|ih>~KN+-&r?Q$;Ea_k_Tm8%J$`*g-pcL2*N2CSf=QO)ZCcX+Fw$x{>YAcB8p zFD$|44;hEJ)}f-(4bA^USV8IX7C9pFG`I=zdWCU%l|0d^Wybk3WsPZbmJY@OXP`ubXOPIk+7*P~S2t{HW4YBN&&#wnb?j5TEUUYC21L+?0J zu9n|qXU0A8KgKU@*7^-c!j{XtAHyE`C(MVbhp~c$>(9%pIdKVx{wKM#WxGL6vG0gn z)-jQ@O&!~d{YT{4jvd9(($cg}D@c@Ym%DFl;w}6c6Z}~7gcvb?V;N3!MhV{vWN7Xf z*5mT+rD2_K7NYp9>}UMjb#yoU&`fz_UMG07*?Dm^OfJv!x%OG;U3m>?cOMBe1gQ$xP>3M~^O3}+e`KDgO=_dJNuaD~YaJ$z< ziaG)3oK(&5jPr#zAl=r*!=t(}@5w_P@)E()7C;(_*>i zt|aO&x8F4dT*@5K8eAGaAcO9dj}J%$uQm)wK>4i!ZR_8OLoazmk-XSzVg7}TgBSlGImJ&_)BQ2_lALX^cx5pdHceT6I-GMqDvn8}%%!bB-d*fp3)g^9cd<5n0wh><-;ju2-GL(YDY0MqHGA5y>BiwX_`f;7W9m&tfG zR<|GSYY$BNSXRhxV@qS+-~Ddfrnf&8zf=A9^81cyuYvXw{h(Rgx`9?R`g~mxpQJWlsipqh!KZR%!!}k3Fm98FwVlf5zt^%&*G%bHZxU}X_y^8oSpJCwzkZmKO*5o#YIkvTuN*hETht63wL%~s zLMZis+&wjkX3Ec|`ry0%m})ki;T1O3p+Derw1uC-xuUJ;2vflQ9ADXRQzu;Go@rL2 zUNUR?+bm6{V00I#(=|C@dX{WGp5Mrn(M8xAoq#`^*Z|2^BW-e$1gb`|-(Z-Kc z*F6SJ%hK3`wZ*^#W@ljF1+%>v?1kALS|HzkT z_6=ZRes`m6U$|sF!H7yt;G(ZIKfmyVG^)-{`4# z>AR10Vtm*We%ewx^@&jtw72w=m0vmN1G#HWYDCE%IHfQ{(=W?!*NhOaZI!*B=?v~Z z@yuZS{_spT+wWSh?DA}Fl+0(F$l|q5Ise%@3Yvu9u;M=Al`Zn_;!a_^D$rRY{df6n zaVPQf9{ELaqN8e0rF?i@vP^g`3zctq?lC$fXRY(ds^@OP0G-y}0PQ)ywlTV`W0xwP zb;>W-#we%*eo@D{7^);MtWA?oJnsc+>+{{jg+21}^NF}#<=)z?!#(I8Ib>a;IQ^QO zy>3S2VxteVIdr8G7)79VVp&0bD{aC*MnRBqeQ!&PF@94|MfxiaHr zk0NE)bW{5ahl#D8wo04VIjSr&K{CK?FwJ|#U7KG1mLV7QGbYqUV zyjezV;$bp3-6KAJMV{VOKOC;7I2tOuL}tB`CJ%4wB#Qn@_%GTpxyR=E)Kd=IT$i4f zGd6d{1<20LZN(oXFKzaVHKlUsmiuUp9I_=vp52m|b_;G6prLv%%(VoT>D2!K-;F{i z2N4%SfymSJr)86^ccP>Gt-1KE+&T^2mFB#U z8a5PTa|J(T!`CW2`GTWakI{KDAAf{V&GZtRSI*bR)M9?%vL92zT`%%{Is5h7!>(?v z;OM4MetxXJJpX!jCi)6XhyCN{CJwOSPf^luGp^EPB))YLMt(GZ%fjM&m-QD{x5{;8 z-C}x1Mq%eR%A@^0#6WxH<+ASA_MRxaZf_!&mbc{k&ux#B$IBa2zPwr<7x(vlM+(`u ztS{G|_r|PXqx#z$GtYCk53w*eCHIUB!?5_K6YbU1tpw(PFAw{8<>&TF$Br&s^X45L z#f80c(T)~Lx2Pkc$E{>z31mMcSXvZE8OM)aK{RE6|&pDemS{qMLDkv>pL1@ z<%c-0f|0cq`%we{0kjo|6*`-KhPV$OAZVI?VAI8 z>}K2Fy@#HXA0BKZZ#Y=Lx)dJM8k5@};m0r32iJ(p4(8HRr5_%QCVE_+I@G^`b^nM> zUKn~?u@fuxU|Co(8DM*#_ll^q{QkYW#UB;2$NPEIxpe9K(In;xId^Max#fe2@PeMh zyB9C{AA=Pj$&k>Vs7poH@9A+HbNEZFFqVe<^#xpjMd6zTTzkW(Aa?b8^qPYL$9d%U z66nJ3jpE?KZ8XHcbxTwaZ-3gQ-8znHmJ^uFwf6@hL-R%{%(-fe(+E|VWnXa%X?&K| z>WUJP+t7^#sz<0@6uN&p2!tBTgufx?=hjvK`iFAuXW6%1hK#N8lJ#E+SBUHuLvdX% za@7diFapkl$@=Bsyxw7q`&!Hq#)L6Ork>28-(>HTsVTofH4#%A)Lm^I@X!oL`i*3q z%=BO7lP6ZALOjX20 zFO0&0pbu;n!MPr{W0sA~b6VDHiwzV;FQBoF*Mtv&~4 z>IN5Iuk9ju^|N$+v(j}G<08CY|IJ!dzWMB6*BJ}3@oXd4g0B_r_}P>&`%}Qr@{hBz zu1i*xg#WzSwh+35gQZY%G(r&a@=Dh*7WWYr)?sVabBu+!AW!k_eXbG}e9KAS=E1m2 zzsbGfdVQJqDauvOa8H|@pzDo(;wn2WblGD$nL|larG4|n3d4YW9+ypu;qrPuJb7bf5@!7XP4*5Q%e)ahSXIUJI zF=3o#aj|cFU)LSUUN>g4QL?*I4*fp)x>Idbu&(^(`(zx^TtAf5{`R7QJAM`~YgVTA z?L}Gf!y~uz*CdV>BFFoK;#&nD&9}vo?oq-qM?J^+=NH`KFrLMn?>2Oy+Q%Z{FELx} zVqv&@%EcZAeL+a^OC>xvt~McuFc52SxMl<{k{h5NUlt)-@Fwqxe3#A5l92EFRgSpO zx51fT!C~CoVhq+qcVje$&*!*Ce@K3KVSsBJ*XS*?evI|L&Id|-|Bmy88BYEJ9*SoW z>;aTFO~SSE=pQ>YECWj5j!QXy{xV=4%XkZj5_$9T@!qId?)!0sYbkDqVYHbSljAP) zmkVAAKA-S4B$mVXIFHK_7u$<)DXg$Vb_JHnbE) zem2A?kCX4@)w7`jnfyyUHp+ItaNIcbm-NPe{^A7H#Mu}Z!ptsp6}t&Ke-!*Fpci z)cCa(g#~NKS-%Zq75L(}zp6mUm0MZn{jT)jY|WLSoL#-rJvI1lht#{Bs2m6GeptJd zZbIVtd?&~Lep|*y7G0QeViA~(&vfkt1&Hr>^>ID{I}>Pw$>X2CO#bWlQT!zs2kppf zz61jdgFoai!6W|IMboAC>U>%&*I)gB3YSl(wto@8@*OlXbU%N3*}uw6BWm8H!1{8) z7t>r+--V0?T;dYea&Jxrrrh3?0qqGG51#_pM$>}F{e_ge7%NzTX7ILf@ zmbdfecNVNac<8^UwS^1Rs~WrzfzHe~?{H9;JMB*#7|Usdi-3XGhgb`L=?m|JP5}oU zV}ycB_+y5~L4V&ww@*uKD_Zk;)z}EUhrt%`CriyA95lIhiCXv>Ji6hWllqcc3R)TYPNN*{`396(bw)8m)j#BJVM&|6u6Z1M3{(B3S{;_aI~bq+S;#yW zd~05QD>b1lf2&>QSQp-$E-@*T8sEF^H@wi(v>ZxQ$TOaleR>@zDWQN*rFNacQNwAe35*^3~?{P#PpItt^WVqk9Pj z^MP>sgkCoLMo?YaXpW8`FFkG+M3ArbCQJdQ)Rz~_AG`~Bk+`kr{fDo;*v^vJT_Id{ z8%vmVOUy4KXq3qRdHD=#^A}q(A92$(vFnW$k(4u7?M`r^{CuSPvd#&U$8O|AuRQrk zpEJr~6fS`G=X)6JC3qTuHvypVF@@g=R9_H%SnBOQG@`5EgLvt!uNY$J17K`tm%s0@CtqaW8V(1Q9Wx915N3WO<=(I0% z7k>vR-)-jAqGq(!oLGwrXoXo-izd3xvVR+G-d~$iMBxJS@!I48X>)DLN!f%wiFXS1 zbjJ3@1KU*as4n`5X~^J^=9rQHry&a#8vX)5avP<(A;&Hn6Ba;V<*G{ zb8Zeu9}d^ZFeP1Nr;^XpM|(h2p!NktftdoALC zf+5W~UTJ<%k8Tqy7n>=ul$mmrYvSSzb#RvD??#Id+}L_@tkN78OR4y>WG;)P^t45+ zXy7D&utGCc#!CktAPT_`OV;?b()=Qp5@@-3IhKaUzNFOYI?f@aGAGw3&kgvJ3(Zs? zz}WrkuveA~n@16Asg*}`r@c_%2MtMF9+BH@PPzb7{7otB9u5D zHuK{s-Bk=O>I=kCPJK6k!HQJ|{F?S*eZVZTLE4d^wTgVLWdoQrlQ71hwoIvv#q|!j@1g ziH;|3M>wP6H;#r;!UhDRhjMILY&sfHAF;p6yrTj2Pu+qg+VM0imD0kqLB(S&JoS@# zumSagvBf7-d!N;@)mI^`>zcMoNedU5(~_w#Th2Sl)KzR*ZAPcSVIsdNg}T=-!6v|_ z?liVpC4TX+akaTNh1x_i$`i_a1u$Qw(9jsJ&JI$=0YLWi(A?-?A1P*WuJIyCcjcJ`=?TBj{U;I7s?ziF19AYyN|IEKwbbVGS+b^KYsy7>dY?G zGdHDD{lGR5_`(Q*NwDNL3bd{h|eI54sdCys{N(d8yN7c2OThusEl zD9muonC1)RZGbz?PooZ*>IIA9@nK9*tqC{J>V*ocmz$ps5+Ax; z!N)p2(F5KeuL$_+R(M=^ietUOvEJ&a4vrR?6Pi-9ezx{ukP?gQ?ArdXT8ITk;ACq| znrxidaBM@!RF=~JN}gnOPHX`ZW+rlte5-RG-`p5g&zem$C`p|U47-j}W*j*U z=j14-$Q8MsP|T^o9GgL%#JXa$B!k*TSN8%EI{6`k{CKov-rbDGyOc33Fb_4Op1Deo z`3D^IIKc&;A}cz>8knO1zRA@vIsT_O!~AxrQ3qsFlK`6$qcnuX;A5{u7C9r;VgV+1 zeQ|-=JBzZYzzk&3oRNRqdzdv3^j`DCZW$P6g@|ze%wa1YF&Gbwx^wwk89ZHV4r@V! z8`@TkvkyNiv!sa~NZGIm+VEE`@MclHF7s+i8DO0EwnQ9>d}d2}y%zI{+hSkq1^orH zI=7;PNX0(oM1dLA3jM;tFK$J>?g~DkbF%AE59E}0Q0~j_Oj#$(+<}Ft7jn!-wrh;! zQ4OzmD+XLcMm7z&=6PIw^SNwVr{2hM1FY-s-^iJxThpdUix-H^*>9z^p;-2sIc;ca zn!WaFhb0X1&@^9&igoSX1H3rghCWZKevtP!gP8}~(xL0ec`nBq=Zzei%1*9%JBzQk zw6psXJn@jfPTcI$p5C^-uomQtmoWQkAM9}kC79KLvaNT7tntjL2_0xqf~}PJPl;ez z&_g9%7(4vw*BvM=$Vs#I4fFzg-t9L~SH(^BU%LIx0%5-3gQp`sl^hhY+Fkx4V)JlE zN(j!ndz1@PvQ1whl@q|zOd&7g+WvwP66IO)x^IjQk1owj({2s{qFaXZA^K*x)5A07BBCzVUO&RF>Q zDK}=9Xg?HR?z8ZlzZuxX>FW{n>^yv#e8dbw&v)zh`Pnx-Wog&=v&{E^f+=BoYI zr-6a?tq3`2$b!`sX4T;bYGih2uw65hNALG)r zde^|q{{*l26Fd!r*XH_VMfcI88AuU*H2FKRzKB`=cjV+^&2S=Y0Y{8 zS|lR{%4YfL&5&9rYw6k|zzIlgkxrKNoCA8TU7&JrIbdWa=(={jZrbztP8hCED1$YPY3`XLw6flBe?Uapgrm!{m zuWUTm;lFAD6*(>4On_1Ympk+jW#!;zQU`1Xv)_*xG%V8RZYfD2#M;9eWQ+s5Nn zq{5G~2dD&|C)Cl#zXcdCqqUFi`kie2OuPOkHvUE%KN0u<4}8)lbhZg!0%kq`(Z=W6 z_)CCUG*Kh0l^Hf(2h6&XZsYT8duHh(JnT=y^ZfOLJugyO*WxB2t1)a$eDFr zZG3;gtVAPhd^a0E8gMkq(~$E3-EDjsbcp+zY1h9QhKJkuKLE3J#E!E1A7SI; z0UH=!yBh^=Fw!Or2HXJUS$2a_HhvD^9w@(K*B@=;4+3tC@HNSG(grvR*7gB*N8@b#IpEph++*X%+xWqN>!3W@#`6}$ z{k(ztY{t`oXQiFM{+|g4K;Q;5>;@B2W_&!9i+%osc7sVa{-oW}EE_-B#;*pRC;G76 z&lDS<512W+#>P*z@x`M9R)ftpVVX_&d8Q?^S8e=s8~+;MSd&oMOi861!a}YeTNBaSZ)Z!X=70PV;FW~9X z-R4Kzsc~R+gcV;7hgjLDg0LSjOFjq>0nA+o;kJNT(p{%anaBq9XcXKA^1hfj8&xox z{u(gHN!P;O8aP7x1Qis*oWFx6hEzgQDdsObD5IV#qAHt85|TyA*-6dHmhPlEj_6cw z7pzrfPTWZi%!mqFnElj$D>ZkRU>@^EXdGaeP&ls&q#2Lkzd>~q3jBZVasSb9kGZ#k zrs@NR#D9?g^|GX$$DZtScdKuPnO!R>AEEWzl{ALjX7jytL!+~^bfp-)DjuQkY+bv5 z*w_gjr%#wL?EXKU-D{(5{9P$pnirsDg z>mBNke%tQHa?j1x)iT**@E`XbX3p486Kl%!C8fJ@7<5 zU0HG(C{alK$2P*6@*Gl7WXt-D1mA>7GQEzu=w0eQ z_``5r>kW_Eu*1;~;Ga?0i_) z4k3MxbROv{Qp_A(^C7iHGLU*BjYOJ+Gz)1N(t4zANPCfvA^nDwjh&(=(g>u9NHdY< zAw7lk5>gq`TS!Nc{*ClQKu^=c^RX=;HAd=$)DLM8(p02;q(_lfls$BaGF-e4{F$4M W(5ia;jKm!SA!u9UFm3!nN&XM>i7Af& diff --git a/software/file/native/crates/file-cmd/src/lib.rs b/software/file/native/crates/file-cmd/src/lib.rs index 92a9faa647..38f507dd73 100644 --- a/software/file/native/crates/file-cmd/src/lib.rs +++ b/software/file/native/crates/file-cmd/src/lib.rs @@ -194,6 +194,38 @@ fn identify(data: &[u8]) -> (String, String) { return ("empty".to_string(), "inode/x-empty".to_string()); } + // Check for shebang scripts + if data.len() >= 2 && data[0] == b'#' && data[1] == b'!' { + let first_line = data + .iter() + .take(128) + .take_while(|&&b| b != b'\n') + .copied() + .collect::>(); + if let Ok(line) = std::str::from_utf8(&first_line) { + let interp = line.trim_start_matches("#!"); + let interp = interp.trim(); + // Extract interpreter name + let name = interp.split_whitespace().next().unwrap_or(interp); + let basename = match name.rfind('/') { + Some(pos) => &name[pos + 1..], + None => name, + }; + // Handle "env " pattern + if basename == "env" { + let prog = interp.split_whitespace().nth(1).unwrap_or("script"); + return ( + format!("{} script, ASCII text executable", prog), + "text/x-script".to_string(), + ); + } + return ( + format!("{} script, ASCII text executable", basename), + "text/x-script".to_string(), + ); + } + } + // Try infer crate for magic byte detection if let Some(kind) = infer::get(data) { let desc = match kind.mime_type() { @@ -231,38 +263,6 @@ fn identify(data: &[u8]) -> (String, String) { return (desc.to_string(), kind.mime_type().to_string()); } - // Check for shebang scripts - if data.len() >= 2 && data[0] == b'#' && data[1] == b'!' { - let first_line = data - .iter() - .take(128) - .take_while(|&&b| b != b'\n') - .copied() - .collect::>(); - if let Ok(line) = std::str::from_utf8(&first_line) { - let interp = line.trim_start_matches("#!"); - let interp = interp.trim(); - // Extract interpreter name - let name = interp.split_whitespace().next().unwrap_or(interp); - let basename = match name.rfind('/') { - Some(pos) => &name[pos + 1..], - None => name, - }; - // Handle "env " pattern - if basename == "env" { - let prog = interp.split_whitespace().nth(1).unwrap_or("script"); - return ( - format!("{} script, ASCII text executable", prog), - "text/x-script".to_string(), - ); - } - return ( - format!("{} script, ASCII text executable", basename), - "text/x-script".to_string(), - ); - } - } - // Check for known text patterns if is_json(data) { return ("JSON text data".to_string(), "application/json".to_string()); diff --git a/software/file/test/file.test.ts b/software/file/test/file.test.ts new file mode 100644 index 0000000000..70657e05b8 --- /dev/null +++ b/software/file/test/file.test.ts @@ -0,0 +1,152 @@ +import { existsSync } from "node:fs"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + NodeFileSystem, + createKernel, + createWasmVmRuntime, + describeIf, +} from "@agentos/test-harness"; +import type { Kernel } from "@agentos/test-harness"; +import { afterEach, expect, it } from "vitest"; + +const FILE_COMMAND_DIR = fileURLToPath(new URL("../bin", import.meta.url)); +const hasFilePackageBinary = existsSync(join(FILE_COMMAND_DIR, "file")); + +let tempRoot: string | undefined; + +async function writeFixture(path: string, contents: string | Buffer): Promise { + if (!tempRoot) throw new Error("fixture root not initialized"); + const hostPath = join(tempRoot, path.replace(/^\/+/, "")); + await mkdir(dirname(hostPath), { recursive: true }); + await writeFile(hostPath, contents); +} + +async function createTestVFS(): Promise { + tempRoot = await mkdtemp(join(tmpdir(), "agentos-file-")); + await writeFixture("/project/text.txt", "hello from AgentOS\n"); + await writeFixture("/project/data.json", '{ "ok": true }\n'); + await writeFixture("/project/script.sh", "#!/usr/bin/env bash\necho hello\n"); + await writeFixture( + "/project/image.png", + Buffer.from([ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, + 0x0d, + ]), + ); + await writeFixture("/project/empty", ""); + await mkdir(join(tempRoot, "project/dir"), { recursive: true }); + return new NodeFileSystem({ root: tempRoot }); +} + +describeIf(hasFilePackageBinary, "file command", { timeout: 10_000 }, () => { + let kernel: Kernel | undefined; + + afterEach(async () => { + await kernel?.dispose(); + kernel = undefined; + if (tempRoot) { + await rm(tempRoot, { recursive: true, force: true }); + tempRoot = undefined; + } + }); + + async function mountFixture(): Promise { + const vfs = await createTestVFS(); + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [FILE_COMMAND_DIR] })); + } + + async function runFile(args: string[], stdin?: string) { + if (!kernel) throw new Error("kernel not mounted"); + let stdout = ""; + let stderr = ""; + const proc = kernel.spawn("file", args, { + streamStdin: stdin !== undefined, + onStdout: (chunk) => { + stdout += Buffer.from(chunk).toString("utf8"); + }, + onStderr: (chunk) => { + stderr += Buffer.from(chunk).toString("utf8"); + }, + }); + if (stdin !== undefined) { + proc.writeStdin(stdin); + proc.closeStdin(); + } + const exitCode = await proc.wait(); + await new Promise((resolve) => setTimeout(resolve, 0)); + return { stdout, stderr, exitCode }; + } + + it("identifies text, JSON, scripts, images, empty files, and directories", async () => { + await mountFixture(); + + const result = await runFile([ + "/project/text.txt", + "/project/data.json", + "/project/script.sh", + "/project/image.png", + "/project/empty", + "/project/dir", + ]); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(result.stdout).toContain("/project/text.txt: ASCII text"); + expect(result.stdout).toContain("/project/data.json: JSON text data"); + expect(result.stdout).toContain( + "/project/script.sh: bash script, ASCII text executable", + ); + expect(result.stdout).toContain("/project/image.png: PNG image data"); + expect(result.stdout).toContain("/project/empty: empty"); + expect(result.stdout).toContain("/project/dir: directory"); + }); + + it("prints brief descriptions with -b", async () => { + await mountFixture(); + + const result = await runFile(["-b", "/project/data.json"]); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(result.stdout.trim()).toBe("JSON text data"); + }); + + it("prints MIME types with -i", async () => { + await mountFixture(); + + const result = await runFile([ + "-i", + "/project/text.txt", + "/project/image.png", + "/project/dir", + ]); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(result.stdout).toContain("/project/text.txt: text/plain"); + expect(result.stdout).toContain("/project/image.png: image/png"); + expect(result.stdout).toContain("/project/dir: inode/directory"); + }); + + it("reads stdin when the operand is -", async () => { + await mountFixture(); + + const result = await runFile( + ["-b", "-"], + "#!/usr/bin/env node\nconsole.log('hello')\n", + ); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(result.stdout.trim()).toBe("node script, ASCII text executable"); + }); + + it("returns an error for missing inputs", async () => { + await mountFixture(); + + const result = await runFile(["/project/missing.txt"]); + + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain("/project/missing.txt"); + }); +});