repro-threshold-0.1.2/.cargo_vcs_info.json0000644000000001361046102023000141770ustar { "git": { "sha1": "5648b1c227c53617517e0c8e50bd767fd26b722e" }, "path_in_vcs": "" }repro-threshold-0.1.2/.github/assets/screenshot-apt.png000064400000000000000000000523271046102023000212650ustar 00000000000000PNG  IHDR|tEXtSoftwaregnome-screenshot>TyIDATxE`BQT`"(`Χ(9gb@}<L(Lg@̇QEo=OϿwvzg>ka{f~[u0w@`Z#/4YV@Z9%\bn&X~۟E]Z{lj9 b/ 6fĉ~Zje^ziȯA:|=z &[<1mO>1[O>ffM _~vԵZ˾8p/e.1b1c6mL˖-K=W\q4i9s;M۶mUW]ծ7]t)lf 3=}<ͳ>k{v_i?֭y7m9^]{^\?SM<ĕ'|3<67+ 7>tCA 4vm5GqDfv3+ڵ9sx -CzO<;lZj)sgu]{f%C7deEE f<@;,W>~{K/o~N -PL:Z=+VZ tgH]|ku]״iSxwvСސh h ,`|/i>_"%ܧ~ʯ ijʕ"ES:tP;WÁ5dWO"zꝻmpϝwyw\|ޥ^~+M?# Fu9T/>#G(I)\wk|_m~Gw^Uj r!j^ftW`2;2eMCæl 74p@oéU͉M|^޽{,4UzO;4k馛m٦_5w]_qQGeFeGQ7 2dA\^Piv?o N:YYhnׯ^hjx0BŀU_xz@-}'4Ր[oY @ 5jZk| + H- sM7uznYG:MQj>BAV7os?΢˯r]?(RO]tQDiO83C=dГ<7z$zX̉'hP;=?nK/4AY[oMCTESO5~̸qL=Loy[j_ s|duuיohۘWq~6tI'?`fhL<5ȦT~Zj)ofg1f2|o 7]vm6Kߟ7}.y%oӕ\o͗_~i9zw-K%J2۷7@sQ/`袋afӱcGsYg]v4/3 ꫯ i=m'Ҹkgaf:7N;4[Wu1:VΝ)bsO?Ot_1b/O"OQLK\`{0W]uUۚA<{ycV[W]uRb:ʶ0>SG< -+l nʹm.ItҥԒy"tkD{-شϞ=ۦa]wu_4~+0&M23g4wygENVO?\ve56r^kti>.qs|ta5jTlvZI 6ؠ̵}Xveͳ>k{۵kg>LJO'ܕ?nݺ7|ӦȑQ{!?ڷ̘1L6-[,-W?߇ke1O<o=qRSߦ_7-үy뭷̡Zz@~a[r}7#ovZ{M)}G|+]{|'Sz/J78mUWDӎoW{][4j/>+IO7LQXC1H ks+}vߕCvޝi*h{ kݺƪjԤ߮ӱK}k(N EnQLW_=Ӑ݈*wGƇӧjT>i[|륿uϞ=gF1cءt߿y \EY 믿NUhȌZ>0doojE['P+i7 /~xg/¼;7!~Hj.cǎ-\YwCKzU*>Z|ZTyqmWOQ5i|u'Eq=~{k{F^(=z zC[=QK7ئQI<#Zoׇz>?x[A K_g\?߮O=h#(uɓ{뮻 T0ƈx,O%4vm*?Q˸9 D Eֵ4^(PQal~SsU L*Oo"+2j!-}]"v^Gw-].j-9< m.IҶϵh~kxT]#QKbRi^=w5i+=~|k%M~QeM-WYwqί}/ZoחzߴWmQ`O?^K|%O֟~a@@S"T޽9C'lH;]ۧ駟F+?75H{5̬:6@ӫ_i,5Q]_y*O#TDA;huwr_]ǭjz]mT~+Ejԉ^J>#[|]@"jщ45hZoZZC[*}ާ9^Hut~+Z5.aJqJ\"kOQa\=.}Een֢sCǣޣSe_W} ԫn1VCvaN߸(BSd H4Ҥ*_y;uVx?Fm!yӗ|&S<}qн"zQ+>}=cvjc|U*b_A)`ɢިe[4j0h})҄$Rk^gOsr4ǯj>{g/kϨ579i+|NBm԰JJoWVlDyyը}E [mU+)2|(UDn ]5|҇o$ZTWs*u)pRy& >5zJ:}O _Wu3枿 >֩>_W 1 .ŕ>֩gW^AHKk/3a„zS,n5}i,Wk=_ƹҧKǐjxkyϮ徢MVI=N1]#;N|jQ ATn=RyyK).R}ZFVK<_枿 蚮ۺ?ʇL] Ր`0ӝ`}|SyTKe9NR/÷4>7}NP]p-BTVk!aձ?F!!Oy>VECGɛjoRZECtqnBAV7os?΢˯r]?(R@F+0P;=?yê[uG|~rǖ/4?z'|R"]wuoz9q6Uk tI?4?͛JwVoɓ#pӵkiK/y74o _x9Uя ƢGVh;찃Aӹ[mAs|K/j97ok3G}9/nƏo[o;3W_}eMc駟^~qI38tўv?1:VΝ)bm-ꘈK?׿̈#7}yy@yC=dnSOٳgV]w{j!m -+l/8vi۶m/}I̙3͝wYou뮻w.[Ό7ζ@j>.qs|ta5jTlvZI 6ؠ̵}Xveͳ>k{㏛vٿ6/.]Z@5Ogo3h ;b@ܕ?nݺg*#G,叏Z_(+ҾUeƌfڴimٲeitW>\燤_,y'w}g[o9hM2}/W_y[oC=7>öoFN;d{z+}yu|*REf^-\uUiǷ+{.}ꩧ-Z4ȣ9PC(*7h#~믿~j;{mJ~Uuӥw :,bRTZoj 7ЬfnЂ;v.W^{ѣdM̶n[oXwa+kƢ`Tb׶CzTQw%_~˻~QV7FſSN7l^bJ ;r-o+urW?{キc=J+d[z)ϓҤJ*rj{G{O:?"I/Zwei۔{~IR_Pγylg}l$E]V5S=_:-۷]J9]w?СYz2_msOҺuk;zJM>kK.ݷkm븹=O8PŤ]}3 ٍjJf%Gym|>}Fs]??OuȐ!h@9UwBVZoojYd[jϔ)SlA|9.H}/n;j6_|ywloB*|ur]Pǎk,ԻʥZ= ?XUhk^\|ӢmToMϞ=M|u'Eq=~{k袋z pv* ۷oT]G:/U7xcFU& #T\WWjY$8m5,}^_I:}>6G|%O֟~ST6S! T?ov|OA?lT~kx2nn_P Eֵ[j|2Xz@ fj`Vy|i_qaU!Qu#ƼQ5HTFĉJ-S^UaM_-jg⟏*S`g}Voy]mka( G4+J7'J{RjrWT{hyJzʚZĿ[hg\u~U:\WjY) Vݗoh~Z=_⓿.yߵO ]}ե~!bN>dG>O? 0^p ΍=E Ej0R^('j묳eU)?4W4EA(XNu/|׼yjHp4Vi} Wt赆 oT8kXD d;^AWrp SSτxT>AyifOqmbe+=~?^6mlk*i:THWYΏJW]!+Zou~IYsC|uVhMgzr;}.*Zk-{nx{뵖J:}OAzUՓ;jȮ")~YhUFTC+bVhx?Fm!yӗ|&4_UyKI4 6_?}&^4/TÁԳ99_5w3W\uih.tn^a/}tYPҩ!>"UTۯ6T'ӎ}FI-I]{uZ76jRw7+Q+ZQPڪBؿi\W*JuYvuI+k~񩊹TSK|njc+7(эkZ7t|lJzO^ϼ{z6|p*\lvvA׿`Piw)yݰN=z B_^{ &{etKte?s&lztc!y"%^Rjw]U;K8Ww+Tä" 4uаU4'Ntarݭ|~z64F=zb>#C46\sMWkNTҼrW?*>y\QZ_CAqATRKnFMZE -qU qSߦ_q_iүJKq *TQwv}^\i|ו<ؿS-篼Ԝ"%>ۧejdU4S"4U뚮ۺ?ʇL] Ր`0=^VCy:msnbU͖/~t?< } *r>OׅVs|Dr4/znxۼ@^gVhxii5>=^u iY7HB9qҵG5+C4DP=m CaH0 X 5sXVz=#5yDE߫|/袦hJھU|XWcTV4;zZ5Zje g҅,iKt_hz~j֮]ږw3g~0on>q>~?O9cK=u7\fmfy3k,]8`T}W^K,gy;|K,aw_s饗30;vN;4м1"/뮻W_}e>Sse.[uUm?ntRji|_ -+5n3m۶--?<￷j׮W砞yfԨQ/2'0}mYve㡇2ryꩧٳm^/gXN4̜9yߕ?-7nP^uܹwOrK;o` JT9Rݺu92SG]}i\{fƌvzꩦE "@Ǯo}~ڴivl.s>+y/S~?G=H:Oʛ4jMNJSyR)ڠA̓O>i˲Ǘm"OڡCoּK78U>OW]G߼+ Ya",`ٵ^k[{a6dۖmu}-^'|njf 7^^ a>v駟nveI[w :,b6fvUh;8ӯ_[oզ|{o{VZOYqΥ%\nۚki,!t*WEW Gq]:}oW_\ǧH;Ko_|lFikZcnWcVՖ\s T}4[>OW^󞿾Z07+KjT+h}u!rM75_|QkZ<9d z2SLC)Oͷz˾V:{YHtxmV?# ޗ,M5ۯV]/,U\g뭷g}c֛C\믿nPkf__tEv[t,_֟OK`1Yѹ:MM^>r}ˏwTol9ͅ^h+_b@X$ڶ{ Քߝv67GׅU"<_Zi|$]UYPKqW[]&<4\H`!N-% # }_p/iYTSK*o~+T!>,ۧmxLۧZGZV'@+PEUGm@6DM޽'"-?Ws=vʁj]"uE=I;<:M)_۴icEZM(E\kyU{} sL8q0o@օ9>p,E{?U$ͫ)yKzIʟw\i'j<ʷ믿WaP$>wO?>wm掩_=-B#fDy\uI}'I_ToiH$}vD"iwD+^jTSN>ο9Eզϧ|ps|>qZ0˺^{}gܨ Ŋ+hoT~jy޽M _of 5ćO4T }#bO>.P4WERs97= Z^Enl6#-os4TJ!CLp('tRmt ߋWl?>wm*󚟥>h([= |4KR9'}ҟE?owTsӥ"'I_dfv~Me&\ ,(ErJkHnn6Y_m|3OC~ktin{aKàjxcs =GY=* !šo#Z<պ@;o!A !LjA9GREC!,j1$E sX /[/O*=JA?z} mM-t,}u6o7|iٲeUT!oEjX[y7D=,׉w>~7z=N|mg/]we:vh[ꖪs毼1c{!o7c=ּ;~0cƌ1K-TvҿZk9A~|׶#IM=i[NBGЉ0LşU8IQFM63CC۳.G6зoNO_O=TۭEY~v:Svȑ#͹瞛s2d.=k/ԀO#wwg}֬6^֑T6É'hK.,.k05A^Ӫ՜yv̩2 6V/o_fRv /쵼cArk.V2u3ԵNiEjfx2!?YaJˣ'_Z:uTZ;!i]Yl6Ms-`̼zGUi>s5ر_ݾVk?>?m|'jy̷~k hǩ5{ ʫ^R!u6O<|Gsry}3g'W_]9;vw$w-Z|ܕ?mۚo1&L0{6j~W*5GM]~m[:?\Z˵LvJg%M3˷qGZT WzMfnњoǕy/{Vyxym5-)_첋yu~Kz.hp Ƈ+} uL#7%9]o\U*Dt1|wΪ>t!۷~ƵR֞uغz]ҀYemkE#^lTײu]a6hTs`]kRke;J/6`뮻Nj/hjݺyG6$[lNyԉeC=dO? I|>]ۥGqDD&9sO7=m]4l+RFtY#Fv[{gtR׬Rp}V>?w_HqC|Eۥrۺx,wr[ѣwcU>J|T0{~f)*_5d*EO>^Jj6VI+~ҧ}5X>v]Ѱ"ySE\~Ϟ=ͺk+:/zҬ4SL=:/6,N 'tRDg2r)6/ȤBAGi{8:Y#׵Wpb٠￷Cu>P@=ö0TYRFԢ47PQ˓ZUS!~W/Gz)bq忶Md4]wզ+)+-wOǏ.ZO%M~)9Dd)*_Iu"^D.icWߴ;q׏}ᇶE=WQe}RtUڿ~޵}*C@_| F/twhgaR0P\ѐ^_`+hQ<']mp|9֕ҧF8噶]U5*M[7}ߤU>G@GeU4dXC|MUȺ^PIosX-7qJ˗i{ݥfj7">U]gy=vUWs6zW)*-S! :YEvb%Ze麞\ce|JԺ'jZTM|D*wi"Z5WI. zµܕ?E?4}jU|+U-2ӎ+!E]ky?}jTEPA~bW>VטK!a԰?϶7|=k*AVp`5)⍵cUINX߯y~UUʹG\o<6T۫L{N J'QuM7uGK/5,YWR7#U5$*AhdӲ!_l.jAOZ& OhZoԪU܆*c=rqm+}FSUJoK~W}ot,V*/,FEEWYiҪo$-wOO%M$YʧJLj?FI u;)}?I?%>>ۧ ҥH}1+*0JErA?~# 4EA爂ˍ9iS`!T#z M4v}>:~}˯ujBCey?l0\ ̻Z_Д5h,ͷvտn5sX_;_;pvՍic k2GV7]N: `d.yr-mE_C:tCV\-VZ hxgVQMC  < C|]H6l3~ O*yϕ> .j ZM Ps#tCX\\NjOh߿k%m,wK>e+=~ZǤ[>Aǎ K ,`7MRTǕ'盾$EK+߿~gcV[:usҗDw}k:5S85DS=qIPDGz'+Ii ue uX⢻YO0* 0|?:~篫|V@Mۿ馛2JTaW,ԋ{8߮WO>֢C ky~LA5sgsی渔៍IuKlZi^o8E|`*N:=2G s@zZ4+ҝԅO}n^cǍgO(}>˗^.g:4o}N2 MS9 ScM =ö&kӞ{*?x`B-򯱎?ﳼ ;rWd9~4/CcI*\c^y'UJtW4HCj:U:\KR9o%y>5oN2C3}4:ڑ=j<\z͕ԏ柪':V|zDDu-qJ aaU]RȒ>]uU ϻ?Iͧ~yI} 4Q=MjV꼈o%$雧!OyLjQei(N^eZCפUeN߿iBzMߜ}Tz ȩ@拨4c'l[I& o7"`0Ӎ t#j#|agěo9}}sz|҆E1$JpXp_#Zn`Q!cyc1D Raխ㷱Ἲ{LA ?XZwyߒn3F2N;dZ*vϜ:|ShsWy^zi{Ѽq5@lyfNԜOwU_Z_S1bD^ܥZ*z(|`Zli;_~̞=>|SOm7hWS !"`F0p@36mژ;>_5,oAdڵkgFi=\sQG9Y5>k׮%wرkms]~zw1kս{wN'OQF/|殻2:uˢ9y̙3ĉꫯ;wA'ka6͘1\ ?Qkf3SiT۶m&L`zm|埯gҸW^'ytuYfzjx׏?Yaަsα[u-W_\u~hoYs5/E*o}6]Js$1vmM>`{ʳwٖ]?{yW1{\wu =U>OW]Gu[P-yj6xcFي[la/aq*}5; -Z0>dM̆nh8sG*>w*P=_{キhBulv~M/-(zQq*?joy__^_ky;`UOZ`ղ^+U+J?l(gK.|ª`]uT뭷_^#jVE-j VЪ 묳Yc5ѣ* /`M7W]ujРA '`+| * ʷOAcUZ^>?` T/nÝfȐ!ҩ}^&X9#R~il[dӗFv[x,hjϦH-7 rɻMVՔj[2}Eyοj㯖o\׼XSÚDTa*?߿]MAT(IQI4$K-e*2m4C_feh>^zwD_e]ۊ>WojyT:oU|L8=3m%QǶgUW=i='CNy1rM7 5UxS45hq%! U5j"ʿӗ{}W/iHih_$5E:vh/rV]]?񛘴o߾}mDw,&埨r٪UF\ۧ]RCy~I p.]ءvZKW+ݡUmJN\rI$G铤OƏo{c[ۊ39Y5)N~h*&M2~_z%;U76хx6߯Ub~0``G{O f_tE!X(>rǷOce-"g~WkV[me*{󞨢9֣\T1PxEҵ'}ҟE?}tI'J7l_|q;W2RN\Vs.yʏ"<_T@-Ի <ضW~L SRePLͯќ!ݹ2TkJM.zy80'5XÖsOJ.pɊ.кцhH}. (zQ)znF{7IQE#9jamth4$Qۧ9z>, O?d%\֟ۗ~׼5`9n8c?Ƈ"^w߽pK͑wBҙ'}ҟy?GRD$;݄( SǁWKW(Mt\\F(PVWt<u|SoRy*\ҎOW^(\ן4EO< } MV*KnWj.Tz\$5`QYe"#Uo$'|}lC\hZC D 5ZXR*F,oP! [4fj>W5YD6^y;LXsW S$4H7AڨzH0!zXӸnlTXsVD RŻzĉ AL0z74k>}ɓٳ XM4_~f]h=XZkK'yM +`>#7U@c7$ufKƍ3o;v)/e˖̙3Ko8M ڪU+3x`3tP; XÁvj_[!];wlw^3e@S /`h*ԩ'ѣGJ+@XA"`U(D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D + H D < Rڮ dIENDB`repro-threshold-0.1.2/.github/assets/screenshot-home.png000064400000000000000000000303571046102023000214300ustar 00000000000000PNG  IHDRMeCtEXtSoftwaregnome-screenshot>0IDATx ՠ?Ui$%4iPETBHKI%n$qÍFB%e_I)]Ji)~{Yg;|y}>~{~w|NW^ʓ;+O+eKե|0w+ryj$$R^R>RgCv/eN!Ov˧:ZK){S )0W|Rj_ki{,Gu 6.[vԷ?DFo;MO~b}oXݷ]'}L' donPH˔P'tkw޲ugr0'A/?:hE;@噃XwGv|EBYˮ)>>;YVj?/HkyKOJyGJ?7}1k$Zhre*0;I7J/~mNN C)y: [M?5IЩ5sYVv K>><;:ai]ʄ_Gw0Y|w/ԢkƚWIHH8G|[) (=zᇗ/B~N pT9^_yw_VwϠv|u- ;tK t_j7:ǛJY{2>h8|zk9c˿{ ` g}n٥Eݟ~=a ӫyXq3Ѧćkzdw e(yʭjj?!wk߱M'ϥ'̓?_[_'xZe+7-gn*Ú-m|fŷ;A%/ufXwhd6n0M~IП>ݝ4T{[i_w߲^{~ߘAxGY__`N2Z@j"l_:qSW`lfmiҌR<6k5]Y9[^[;ͤBGfrg~6_| L?VN84q1lÚ>,Sg'f%C)wUʢ=3"3۳2ƲmvXN4'nS  Uڦg?ل䘽\rI!%Wvk5^ҩxJ+.\_ -Ԥx駀E <.iJ^ܛfĻZ^l=ZW\pXڬ;t:?{ɲml&K&җ/W=h-ʄF)}VxR2rΟrHnKv\u]x~?oo޻]{ڽﶟ2. rLonigrZ'9~ wgt6L);׭IURoAOaj~\ؐڟ\|ޝ/8ڕKyKgjVO_ӽ#~vm].^Z;wߑݫv4'_~tV)AeRgcNӵ?GlO>{O~8 j9jvh*] yC5~cm?c7PM ~d3g@TO˺W/;Ɉ7i8xyX +AޞʚӑÑ[*o/0ךHdH5ν#Ў($UI(ݎ9*$UI̓cm?oJZzTu]WVM?kyLfKM{?~gn?l ̾/6{8C 5mAr5֘Q[n|,Oxt)4 ^2X|~̿y$_β_6؂_Zlqe-P82;蠃3Ag>}~z]:^#,8m]g?[yۂ.)_.}Sn聲ƫ(+o<]*!oZ9q3ǮWk_{MbxEꪫk&3[kz;(_|qYoZOIOzR9䓛?kvmHMޥ^Z?]~q5_r%3;h Z!|O:fݯg=f"cg[^&{|:uvw} /z m:޻K`RnUw|ĥVJxG;㹬{Z94:Hpvm/rE5' LB -Z3&o.}c~}{w}@jzl-Ojʾ5W~GjiS 8R]dE򶷽 vgfkmXL;,k%V}Jk}`_5տ 9?|C*[P|׾ԔI2mHjj&Ԅ|;)k|j"$`^[fe7|sTIA9t._{[޲7>i eVXaf;L_W.ӷf7A<skOhkmXF2_DN8ᄙ֥G}t2.™0\ ~g?kj"S3rM 4lS˒¯'FM&5tFM{;o_}j{z2al?EFFA?,7hmoYyoW_}>6n4acY1lۣʂׄ[625{m#yF Rл 5i^JAef{mN);<.4ǥ/zCf ^3i4LSk^2Lm/}[O K{ekvSsm٦ (oy34'\YAۿWӟ.K.dӔ F"Xs5g6-. moHF?fh{QKٿ|mڋm]E'DF4_gs\k]&"H4M`quG.>4ɥLAc&Y"Wolg@eyo{G,ȫqpzMP!h{ Y4EfR?Oe&c{R{7MjѲ*7 .(_|Qh~mȕir:/}~6A%]=h Zak&Ml\e8묳G?Vo}2\ /sόr^r˒OR6|+\y 8+|klW|,/vaK7#8ʅYʭeF^I s|CmoM$u2dq^`Ԉn>I`~ع$ Wݻ"sI *$Uw#H sUh7#9L6ͭksnQk3;xo}Lt>Fۿg6,(`̨]tQ3Nq rSc5>{#<r{ܬ<# 77Կ/5馽Fu{]sᄏW*oqGs5DbQjW{{ӌ`5gnݬ|n2wn4e3`ZdY<杌eȵa{ߌLL3o>OM׾ 5H>cif]wuK-TSBOS8SӟtAwuW9K,1?=~>c5Zߴ>smO~ #w~r)PGu~wq?q3pZ3zh4uWZǕ6g4:gz.nZl 3teo<6~;CVEY>Qݾ3wy}? Z6?Zm՚q/Q_2[ne3\D Z|>ھ?cl?y~:lی 9ݴɜ]wh̿I^e6+/{˚9i/r=mQSr54 n:9Q+kk nfș_/dl^YΌ>ve)gZ)Sq~F7ɴRSeK?af馛uM6-2v^D|O ?.qjgZk2 )_3#5&<3J,i6sinOYgu湬WN;Tipp6?[w%mM߰Kdoj.q.|ȴcؽA?c;^{ !}z1M|;Нo~1dڂda 8 OxBsUfjz82TZO5lZo>#袋fynVh~^e`B.`kɥ;B/mx3پ7pLO@HT9>ڿ˔5X L R[~>ڼ?s<3nxn+sikھd=4)'hi'OS74KϿ_4_dEfa4[5_lH|h6b74DY|͛#kƚm/#o[پ8y/hݿ4oo Zc6o&믿L?n>y-O҄rR=S{@3նd+_sQ&lRj&MykSq[i:GLSqD#'n-˾6%͐m?h}sBo. /}K˰ݾo{ۚ +@vѻP,W>A?$0/yKfzk_L)w[He'MvڮX۷:ꨦI>dS{Lӟ;SN)si iJt`ω7}{6'ܣ.jÆ%M)(w};6Mib}(siA+8S\=> /It+,vXsuhWmgMx2\nSrǶK+3}ݷBlN*W_}usO 1h :~z/W$vEOhEoozӛ&4hڮXwӿ3tvkA87wj,7`sv8Y^It^ÚX`^rqÓ[G̎&:r"Vjar43MPB$׿?X&>D,M(WirPq`Vj$)0 @A*uz裏~}@3,nWlnV.s}XǒvY|a}nFNsk7,7rU{]`^FyNn}E?M[侏}&8OHΟ۳>[|n4UǗ]vYs/<^#%2Fvn:>?r1zs9g Ef* 9[l 0;>[En>袋67Rϐv/dl?[bڃ[o]fZL^C)/Cg3O52R3ˈM}k +=H?,af'=Iz_Nrf~:2Ԍ?Wߍ[zM\%2cԍ\SJf9|eƴZj|o<ɐ{:딻y3Y44626˗q32OWXao~~onoi/~񋛾xߚ/u^'9Mj_zC ~<{:'?i4h3v}3L?sdgO?y#p"'Cjgu,wi嗟#oewy>ַf>v11[mʒK.Y.♞Ob^xMdƮok{o?OsYέ0 җƖ6 '|*'޳"4ArvmW>5'̜ {ɑGY^{F5_B_f. x _or 7,kf3<[{Ѽv饗6;gឌQtR0&$O}x-j$|$$ fwx='t 6aʦn:kZ^ؠ'8I,N_$ '6WB\fFxExoƠcRZ e]vBGBmg~LhL{׻5㵶d _ғ=~0䷿m[neSq~FkBk 25t|%>_$XKo~35y"?5Z4>񏗩0څAw#ű%_J"l^.qjwzAa"G>R>@Om>Smư_u~k5IwӜc.v:&fvb[Lkdj 瞦05ZTq7|zO>qtFIl5"'^stN J֡8s)M˔My Zfe۸f\/y$~j ,Cxrs_:xmۧރ?kL 5-hG;1,#o0A$i07N:^MMpjwT/EFLU3a><3~VFUju mo"j0?9V7_*a^0mA2iF~z4zTTޓ\:{ E ^I'h<8%4/S0iz~'5 Ofq(yc)q9oxfAMܯ,"Ml{[`ӟg=ǒ%u)؎8∦Nm]ʹƩ,_ \б뮻?+I/5y5ej> _BlДڻ4KHHJ30LIBj@k2p Y/$Oeqe&3K$~ 4g(rOă?N៿ɽrOtiZiG5A?5MoN_Aَ;<57hc '*oˉ^,Yط9&3AͿa˗җw7o͍S eKoɬߠoM?xW̷ƶ{>xB>dsZp9%MþTIx 43HAO~0{ʗ\ WSkH?/?9 Z4cgԞY*Z42_9Cfy-7;k[neӟ<ΏScZk$]wՄ=#yd.и袋6wyg`2m5W]uUꫯ>蠃)2)|w/;cL;?t6om|兄l"G?Zve믿ȚHdGn8eW.yxv IT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*$UIT$"HPE @A*mVV]uoWnD$>rq5`=#l{P.²KN7|s9c'>񉉼m y==أ\l@A*$UIT$"HPE @A*$UIT$"HPE @A*M\c= L?=7:mIENDB`repro-threshold-0.1.2/.github/assets/screenshot-rebuilders.png000064400000000000000000001456011046102023000226370ustar 00000000000000PNG  IHDRMeCtEXtSoftwaregnome-screenshot>#IDATx UyG^AyB"EED "6G E*J@)$ Z*.X|qE 1DnhAS!8ǵkͬ~}wd{73|3_6AA!"uϻ<AA cdDRAA"IAA! 7za*VO׋YPjuj+JU*y6i*5)J=8J A& 䝥J%XSfߖ*$JuPAH B 1/SBDN3Ǖz~RwP"h8J5i")J (?U*mjAUp^y[ώV DR`*Tl{FA}VNآbU l?B~9v\)W핪_O 䰍PF4?C?SjU)պEh KJ ]CZ_(VRRV_)uK-<ѷ=~2R?SiqivC+QkWXR"VX*6Sj;pXԢJs)?e[)*u+4={ڼ"O*&t7)էROTy'_pn߯~L>qR_T~8^χ 26ɇMԵ=5w~eRȤÏjJmxvv]?T)cg׷T(5LvsϮR_v(5+! hK+&mԦw x")L^߬?ԑfUŠAꎟVLXW F(5GJrWe@Jblqf%?)7WT.6_+I$0e7b즕tR=}ޟkOkP1)rBvO*'*QjTGWoxF$/$~&O3ok?&bRt+&S9!;yRϜɷklLʐTY=QԊZULȾ~G/ϥT^QjnVwO/mL[ BM'"Y^=5m4u뭷O?] B)sgb/@ xpq;g)J>K Ѿ/ /^ kCJѨr{>V+&**xۆ߬H,]# ,}R fqʿC|sِݒ}^0@0IxvRRK_S׾coUNN}>۷r^׳Kex o~l7d/W>_9 av)+s3}??Ry*B ?V .T?Oǝ8:uܹٱc_o7?^h94ß#s"M\k}|ߙ-9v'Y┟zJ T;,?lޟk˫bLwă/ ~w>_$_/.Z1b!>Mrk]?-VNL|N&Qڈ#ߕ/[wh"u}':/p"PuAٳG BfX~z:0;3`=-M838[@v[( 6r[+5]~RVlãʊKX&V7rc[ k9igޛ>&ps2D]ڏ[:tDWXZ$_o!R>'ֹm,˶>%Un{AQ,ݭx.ki"԰aCD eæmV++,N[允;U݊%m~8bvV۽l#qJ ם+} i?ZwoNT F0z*5꧶R~' ąi<rG;R ql2CER:w% æ(A|DRAAL$AU:[OT BJ!_DRAB}l2AAb!ȟ'{nUzzi&߾o5jAݺuնm/~pH|U1я~Mt?G7QŠ'D[I"I\T}Fye˖TPvqUVj̙c퇠2 Oկ_?գG%BR')no[/hF!56mTO<+׿jܸhѢ9WV?rW(y.mThjժ.]wJաCl"?O ?^H~]vڵkJJb\?-iiu"YRjm"/K@4H=Jpgڵ3P+VPMʉ.H}U/gi׿u%ģAjĈj֬Ys3GLbRl.'ÇW ,P;vTIPR]qvt_Rw^>[˿ڷo裏ԯ~+=isNl2uqFuV}X=?C_qggyF4?S͙3G?G5o޼jd ի3͛ƥ|Q'OV_-O_לo|C=O?Uoj߾}Uep}Dڶm?^/:siO?{/bS_ov4maM18z4z^[cT_BPWԩSYvکn[}W7 PpBt~ ǖ7g~gOs壘8O$܁7|SW ͪ!.QBY1zB]~k׮z٫W/-[U(nݺ}{ꩧˀrJXfh\L;vei#GV g1 0KTΖ,w\s5 nC ]|G :wg߮?kݺҥǹsf ??믿^狭(08l~ɖ;/rp3uYHߴl{oe|?7%[!?/թS' Q߳@m>9=~ ǖroA߷ϵ@|JGj| $pWo&'\}GxE%r˯z뭺X}@3AںuV^znb-3('`~%mӥK糚@`nFuiiB2VKTN;vXՓ勂7c_A?^e*cݡ`y7`.ladEnׯb~/W?maСz1cF5Kqr|[z6-߂$?"a$#!'|Rْ #wøphu VI1ӧBa;S.6 X^4i9眣^)w^ofͪa}+͛7e3Z(2`I_6|L;~xdB[CMbi"ggjWDbdBVUVT>BŁ64[y_TSpgJ?BJsBlWuZ>W>SX%HqUWiL 9gf%סC>5f>bfK,S )Wnq^P۸/>{l{uYzK-(< l)_e2~ ǖroA?PǥġS?;l2bPDb~/"aNj7T~),Vl8qBOdu A>5 LBh߫{ 6hsӳgO|tޏO~ Df 25WK,C jS;Z>tLǏ]Xm\t&'|+LaJ67W[DMg6BsxП;wyn DUTE ,?l|*I&uٽz_8s=$ Lz7p>}5d* 8)Ω/W_U/=0Na ,l-7n^$:WPXϟ|6~5-.??ڀԟ `~|NruaK`zKwxXvtd gcKg/WC"8ԖYTѾ<VR\aM0Dc_ \?'aG9_̃ϻ'l'Մm-[hR:MrnAWvsA+B:ZX_) [^Mh\&m6C:J Vn$D,&']I3ǽ/ L$Ar?qK B1)|JH75f"?gTA7AX$AAXDRAAEan¬Ĺo2Q ^Ol{u)i W Y(V5R׏Q&Xb=~[ߪ('8\*[L>dOO(,\6e?uLYA2)~aEBbWj, ? FH#.M׾; 5駟k Xˑ;CG`BNh}].X IJ r'"JUB2w\q!ibgˋ/w:]\>|7 B1)|&@W46UW.wU8;6"2e;k%FU:Q;yvG֛o~_h11q[> XxOy-Ę%WqE,r/~.˓B`5$]tq. # #=y]!yU>Gy׿WŽa_ڵ3g??uK/e<߅VZ8GCXCXwϊD%,LYe67+scO1aF|I^<ѫW/-?AP,u[d%#֮S͖6#K?-KŽv9&ñ//3-+(R EHRGx gI3+B7ذm۶U]vlriU[Q 0@[2Igwڴi՞OLb2!e~_Txw=W_װxb-g}V+ +V&uʪ{7|S3>!6 tz+(O8 #t oȑà8 7 (92y?}ݵkWeJ9,7aId zHc!..ԭ[Woy睡n&cA'.m B̏'K _ӥDX_Ȃi[z~ ǖrЏa+k LARҖ-öamܼy>}7M] N8Sf|AS5cƌ kKY( ѫM6<ÀaOWNG 'Nԋ| ( N'N?O4Iŏ,05'=~ ǥ~ ~K|EW"_q)D¡uig.X[m\'u2YV+!`*٘wb5r~ڽ{wS9{He\wAG':9 NVe`r7u5ly"?FD}?dkk-L` gV6<|Wůvj„ }0BTPF+(uEnÀ<- k-ɯpl+Ɨ0Lr_qH|ťFNQv>}  '?ݞ={41*K}ɓTfg#_8MV fl`%D6\acF jmށ5!iԡC>&olI:Vo"cInHPb|)u;QA5jJVg0`CϞ=W8D~C6-p-gZKCWA';^\}L6a\d5o]/kx k"9ȇ˖ǯVI?/ Lۿr߂N$w;eE7Ff6[H\U+pq8rEc{t[XV sO x\N9`ukI7xOʚeرCQy'Rlyѡǎ ?X91ye0wt($NC [m$ok|DmIڵK[CƌܹZh*Emۦ̙-۩S[zmS~X b .T`g-(*-݆pl+KG|Aڿl|f/}j dN]["!V\(*ˍ|Ym"Qȅ|gI"<8p(aze V!%DPpPxZ0J f8PL.IEt\ Ba*-]ISWnԨt +?D? H    '\I#B=_~A9K97ԑxzxua_NS6Vr,- 5d˅ߚ5kVV>?T%C('Rc$-[2~G(#j( {j?փɃ>;Nv+.%+9tP5k\J RVTsU ,. %5N$k׫hva Oٽ{w=Fg)eD*;vԓI"5$sN}?Qp mId=B%5R7|9sftA/A9m"YR]qDk{ァz];D9rD?Vbr{=73{Qj}U͛7!H'&%Q:j׮ b-eڵڲƻjO'+6E0_кukԝ~U-ZIT36 auꩧٳg ,VQn''0J;So~JsSot91w 2~z-S%e[?Q<_>SUKpu]Q;L~\g{>}+4Ϧ?BAKh6,yN2*ֳܧ~ۥK4[l0/:|zWn1~#FI ?Bfn_M`k;C>S-~mk?A(W6l7:jg~g`҃5+hKӦM/փ 8b\PP 0W_}9rd 4H͚5K5lP/Hz!%q@'O'aoUƀK M[-3{aCx,}l{l3{ G];z벚e[W^P [ڀ,.$Lj-&#FпկKe&aڴiUi֯rh ɏ|#KrףGƍWŋI~Lb'XBo[nEj\#L+eoF;9}z dU/JُFjmNV駟֓6XJd0xJllܸQ+`s 09~xu~ɳFV#6'[}.S>'^T}fl_ZFh C6v[ž՛S|}a"{He`R0\G6L9 } ;X袒WlW>Iwc.o!HA'Z._WZ}R ?\=sڲv#޿,EФItrbd#<bʇ9|.Jf̘1*il .[ǕO̱cǔ( 4 :M'ծ~Q'(YXmjXPLvXQ2gd.I÷E ;PoV,\Po`nko`g ALL͛w~~"Y-\#,^E?%\1xxD @PS&|8oa%/ ˖-S?OtlRyUlrtez?9d|?>R [smI-g˔su>arX+e퇻1Nx 1>BZ)Dr_V( zP~Tϯ21vX5l0c` Aa`[g1qVL&IpT7|SiH"({A8ӍSX.0QqyLb@0G|VI#;.~wuI'&d6R%t.V>РjժUzw}WR ϰ y1Ïa vR |59~qM7-~ %pB¦MJ^ڶa\ˆ]ڏBĎ,$ ~ 1>BZ9_̃Db/(BџJ(n1[跸0;8FIϕ|I\Uc:I7Gpgr\NGtA(6w`ᚊ8WPpO߮5Bikt|N}NRF/G(B>`OS.r)é|ڇ+jeiR\AJIDD@HEfJn2>2T>Qj"\״B\`U(YhVIGH |گ+l^+C]vqe_W-9uԌt¡:¹th{=;)./7̸ڦM銋N0u"Y^VBs9^ 5jnf5sLU(ڄ_.r~h_fBXAEP|bc$^ RC#Fd!=zqMI&s 7#僢M$kUu7vHÇʗg^2J+(4ʋ߿KZQ#ug˔)SbQoq\w |"6< e>:dr^wA~>(q"'Fٳ &1mC贠+&.C}q uJ[?DWV. 9l<k׮UGe 4? P&!/ugf?EMC{ | D>L]Nڿa{+Aɝ?tt׮]uu@C&v1ܰaNز"dݳqF L$r"'|2#ĥ[CX yG\i_6];WaV?M6 HdRq)Dݐ:_=O:–J{񛽠TlP.&W^z3?~ :=gWQ@YZjWQV;vT+V2޽{U:+ Vѣ(nCR z_VIOG%\ӑYf 2¥}sŋD61]A=? ?dH4Dz!~0@.@`пGYڿ{ ^x0 e˖33I&o13;[m[˦_!ҿ>Us!lҾ>1dFy'P6g\TK["a˵DXm\R% UV^үlݼyCP:Ek/+#+Q/au ܹSD ߳gO!L X\o~Swvi^1|gZ&'I'̘1C͙3Go~'N?0ۀٰ j?t0|^\7&O \LV^#D?d+.` 3fCL0`ql`݇nݺi0Khtnk`[|æM~+q'LC.e| i_N;u>s=.s7f&q!|A~~7)w\߹'-G?wٰ8"y:3NQ 6.:Xa  hm`ZGʛS{z&(Uk`+-ltR=cecȵl`D۟پF[N+HQ\e~.{jsm\;h`޽7|Wܘm7|Zz|00c *c&Zҿm?)Z]r%z;âl]{ڭwz`&XUغfpg&L:i'xyqǐM Lr}MTE?oo!`XJ8t"٨)uծ'աÍyWEɓ}:8)ȶ?^>b;vr>Rގ(qʀB~, @7ۺ懎W(8{qO|O`0 ʛlD] K:dۊrm0v6xsg۞ʣ>X5YA #S&d.*> OW*GX 56K+% oҨm;;OVo~u%=mw7$oL1R:k0wA';^\}UTCaVGGEr۷o*:tП<8gShA>yJ`+gٲe$$f;v~zt]~,Y͛C&\E9a[:~O5YXRfKasIµ}ۥ0ɰfy8gwûU>m>f @hgaK|01ŃÏ{b9 a&T.O',x&WXǎ#iyaذa}G%|Hdc _|ŌĕIO~uť}b. Ldy6.o6]\d=~t"{I/+:xL\[(2&(A@̖ eR3e۶m/aNm"p9 ~(<XGQPxS(Nʃ/UVRWutb~gOg R8}M>lc¿}v-'NJc A9S\ pB劭l6\7Lmådl}bmn0ssg#/Qa0x1a[2Ȥ A;2Pߕ04Xx&;f^|z-mDԼ;F8D#'d SRX/ik| IM>+*}-`Wa)~ '&orՏaf/}j TrͺQ &ho|t7؆ck$_l'[[>J& dْZSí"L"(2aiǰc5v9V*Nއ+ vϜ3$ *`;\Hj2I?֨R72\OnKMDDR5+|.ŧSY[S'̄ak?i_&#_6PHֶ  -  2AAbQX5SUBi=hҁO ODy?wW;o*#IAAH \/CJb' ;&K/M"!E:E]/ 'rlW^s> s N!3BV&M6Um.$rv>$Q.Ɯ^7PsM^{w!RG=NhM.kK 5؄Mh5Dl!#夾|\x:&vI/]|_EH֮ԷBI"4R6~!F)JH}2bIϓpZBt´̙3c ёŖ/}?]܋D}^~e].>' |8ވAIrfa.jxM{ܗرcG5eʔjw.{:* f͚ȓBG^q8\B_EH֪Tom"IWG]V=zT_ DĔ4ptt8޽{k_VK.Ui !Pr!g0ӔsAmVڋjGQ/#\OYYgU"5!m`_Et"s QUzl '?+JO韆\M /DzcFo֭u^b k 6VZ 7Bt|C On6Y(lRҵ|ae͊PQn׉s*0[G֭[ua= T!MC: ʤo߾`d ҥJ +[H\wuwi_d[o՟d`!zB X˰8aQe <{&,C)RX$٣[>ұC|Sԩ^,2c+ ټyz(cl+޼A&ntMR> ;U\/.H^8:Sk N33ǂ|ff+ {He@\ʗ+"+KLt8tN8\W`K331 oNm/sݺuzشivn A lEȵ2p_ƒa u fbJgBI.lz"$IٚC}>!H>ek`D/~A=u`,,|K^&ʥ|䕅w[<뿹_\ :lujЇ_}Lp>wjE0 ^>s b;M4z}d tض1`nJl/ (' "@@1![|zo}$nʖ&( X8G;ܣp:f'N:H q|ke" -S-YD[%䳫E>K;:(>ߋs޼y19|Ny; >aK6}XGu_15h@[\6f%Lk#?~\KͥRЉ?Wn:vs4:785.|\WFI) [ L阓I]q+Z`Kz%G< *PQ{>Mf̤:pq=bbL\܏Ȼ񣦞njĄ ADV>t(} wP);C60ߤ/.8wyZbM0]T6-(8||h\Yɷ| & X%߉b U as9H'[]lI\(A,ދWd B IAVP s[ B9ۅ\c6P& r_BGyX$AAXDRAAEbm#WY= 5kN6{Mڭd,6w-_B@:. lA^d 8prL4 #;;͚5+P ?r-Lj,ܤoV hl" D%,n'.BD_k!Bo)x[z!;wZ`AtA#K0"ٱcGG u!} P`Ν:Q*y%IӨQ#u7k}']H"ٺuk5yd=؟Q |ƍu8!z={VFؓĬ4_#A}ꩧ2ҁ)֮]-kX@ F$M~8"䉰DLnƔ)Sb_%.]T? Gb7g?a a͛׫#G^z)Rl#[ڗ%mVΊ+h:묪z0!m`_%s=V*%3 »ɟd䃘S~_~eua+Arbq/M$Q˗/WӦM!$ۡQ`! :f۶m3q3gtT[hgРAj֬YaÆMc㔕ѣ>nܸ%B» h<^ 8'OnPZaǥBaS0'hPV~@ip y"#TRo39SW7t5}hWՇ~}X=\%[K.+7`uy!XzƌJo߾O>Q$ֿm>q0iҤ`.d3Nz6QI1kݺu׽(~ɗ?cw} a_q/]ê^zں1qDդI{Lր4gQp^7t:{m۸qc}xϪ`޽Q t\Hr0X?PVV~< lm:Zgm1|M&}G%ֿ[NO6mڤO=zt5ĖOG%  ov\g} fl`%R.NJرcyZAԭ[W+~@_ S}/&G}T+e,85meԨQ*i a>9VH,o={[W*õ^'ɟJ?4h.]Ole|ꏠ%A>Md?wK%"㟄9g;hɒ%j޼y?'~i/MC%&.l2=qEZl* p%U8[ 8xaѿmСe7?%Vs* C##[oo&_L ȳw K.nU(1/"pЬSN@6nEֿ诔|ZgQ' dɠ'=Bdk_nXth;,($Q%AR![n߯ǖ2J~/(DA6lXSƂ"0aJ^ŇgSat۷+80ٶӭZJo`N=: b慕J߂SLرcu]xO?ڱcS֑|sbKw{ԥ` C'XLJ \7epgiXQ Ԝ,mۦwlGy"c>pB L|Nm(ywh!O HJMl9r̈́Ewk C?C8wyZcQ:L$5|/6| \!$ފ l1CЃppGpčXM]w\sH (BV[lI!!CeNDc!ÇL"_X@UHf2a5-]pM\.EfLFJ.!\!ֶ VD @&  B,R}dsE?zTO͚J+?ft`\U7V{)vBo\Q$߂׵}mkP6Z?Ŕ_N?jei%A0D0쭷R^zi P+#կkf͚dȵ~)*+e!V(Kd/JΝ,Xj^x̾'6,%O+[ڿ 5P(GҨQ#cǎA:o`!:6)|I 5PH flݺ|X+:ׄ-ƇoZU~K/!ޚ7o֯_9˔)S2%hG%K}' a#<m۶hl7ׅŋI.Im [ ~mr\7]" #|& Z!5> _~@"drJ4LVYyq?KG͛z@e V{(,XlR/Ϻo߮>(YoڴI?( /\Qk֬Qulƌw 4g֣nݺe<ۥp*v}K@e0&~C5'N'MAo߾&?Ri߰)AϮ Kjjԩokl\`B` %EA0n>C.=f]},o!5I3b5n~N , lMD T}&(h95PPȩ=D&Uj`+DjSNAz;I;ʻI&z \lذA{k^{ (/6 큥|7 {L+uj:N]f ߵkpԬV5jJ|G+$ٚ>|BK}Dž\G[&.ﷵdo.FP9.,! Sߣ>'lcHDS<6pfKO>**b ۷WQaֿ:~y^) [m!aWl:?W˖-x6 j}lS%5 'lއ 'j>mmMV|?*||~>JM>m rp@"aa^ɏ-T7~\p.]d5oHjH)Ka`O 5|^"PM>K,th%m:bڵKOpe'xBmV]veZqQ.+ߊ+ Bݻ 4H[`6la:o^(b+M6&Lq[w~f^$x]l= =u]$ ȑ#3x7pjժ]pJ|4Go믿^+0}ؐTׯ__AqmAÅ\ϥ0qbn{u]]M',OaϷUo߅\ŋ$16ї`7j-m*_a0Dʣ KM7d (pk[o5k:5\-!L&a+V:דZ-]wݥ}>uT5pgڡӱReŶe˖X!QLɋ1N:t^XݳI*ܹSOxYoڴIoc K>L/~_iW /͛uz؃E\ ', `I`Z#\A p׬Y+=[p%na~k")KNm?rbl+lǢ߯«WM ]Ʒ|\NĘ1ct~3`'0Aя~uی[o'C l'Of}Vﳁ`ÊSNzkNAȘ7nܨ7t\ɶvy.僧~m&L2 XQLOR. ЮǏL\ 6aU`K|PS~F7t8~D,-t07n1!}To_X[1{V}'N\?.. e|˷~Xd6o7R9!VOL>L@4q 9s ٣EamAcD  X mahҤIֿfjwy.|,YWl[ٻ*%++={ꭑÇg<6K\ :~&e=-\Դ)wR^5_>"b,ku@I([m\``5૚E>]~/W [y}4syUAP7H}~}Q=dFj/$*y7:=+` Wc+a=X? ӼyNLt1̟?_o^V?*UPpgW\ޟK .5/U8g5hVt'ƒ/8B9ϕwM-HH;l3zwKeV\>l)G%-I\R~ZA/W)=8`Jϗ0l RBq2C.&FqkH)D?WÆ vjsz.B lkUc:'bNdՂW 2{aY[hspv4 1 Z>x;P|"sNڋ/K' 5.ﷁ1[Se5`@v!Bâ`TEo%S˖-S۷o20qĪ\K&>MA'umIK*8spzeBNRMYR>{t_5'AʷegS][|-IV,1NqgϤ$'!S ŻH#I}o.!)J;aiK/8VSV#B˟fU_H/- PCmAA(DRAAEjܟe|t[\# I=$rNۥ=z~*I?ׇׂ~X+n_!sAϵ?WN1O,ԩsX/%/ҾB\q%ʥa*XQ$?]_!;ŬY$198Ƅ{z_¹瞫/_8t^*ˌdf5ʒk>B'|R_^l8Me"-ZmצMW:T6+߹sa"s6{"4,X v )%NM$_ jE!ꈔ@ ʵ ;vԃ)ezaHL믿h۔Bv8̾ **" POFtȼ3gJD&[V'O"PQuF]y ck:21O]meF|@QM%D>J?љg@f2IBX3R;36V%;OEQ1fQKc.6&(}i QR\( = nhM2E˿sڗ K/:묪3!]‰“ \[ЖD r3VV-_'. ڵk;yՑ#Gt]z/W\&.jJ5lPO\?)U AP-l&Xw#(a\Ƨ#cXMj߻a\gXKslSPso[劋~K&1+iӦeu{ Yݟ ÒC#Jz" }'T۶mulfOxb cft۫5kdM'FoW:6v1 ͟-QϚ5Kԇ_dXvғalRb Dr-{**Ak_.3&W^Z.0+ЃV?K5i}JQ 0(X"9R֟Ky~XJ=z?nܸ2GȥOqoV=\~㐪>}Y'L;ŎFc?cƌj2[z\ASVm=iu0UW֑\ae (ۭު?Cի+/͛`cQ&{B3nL?㸾nӝ(-[bP\Ɔ E6Gr.U3?I֭[6ҥK- UtfUٳGK5L d`6& >,YWM_U`àc˿[8%;g= c`&a~r?dq$\cгgOu1|p7h@V3j=[Y>5A ӡC ~!{^&toy_ N5jJC(鐭W|CwLw\L_e| 5A-o ?/~Cj&(c3Ua1`%UǿbGWNSFd yi :"L XEͅgXA`Og_,\P[BfV/JR'f bu5*oϟ AhWտ`A6;>5lǠP\ꇿǿ kR} ?$JlO\$0 W^yJ۠L9)Yf98 F)b?0&^Ltj(AWe˖kٲ>i']L_e| j$o o[TI0 6 VԶ0ڠIC8/*ɩ$[:0xsՃwɛ;V/}Ѫt,ߜVG! lٰB@(#~x'&z,"ԝ#+89g?ӫ7|S:~2!,Ȁ&: i'`jk_K[Gm &LV?(J?û#F¾e Ϡͨik$``v$J5a NC [QQXA$m=dYʼn&١[zɿ ,t8pѯ.SBqbժUz`lֿ]K sO6nk0+/ )mg@!s0rz,Nz@GCӟ*H? +W4\@~oŅ-=Wjv{f V!%Db=YGfȘ1c5FMj߳#2k2{H B:a5섹-](DSV[ۂ  ;-  H  HUdWE"Y=6JOF(B=m~pjP\@?]~4G!?p=vzK,Bx/X_TOlVpykqAȤQ? bP JE6f\HeQCT'iꏋ׮].^ը'M{:ւ Tqiaa.馛T)B0oK/$\`NM?!//ҿIvǎ ܊VԦMziZ1.#z /h=Z_mA, w!Tg"DӨQ# u̙C>_ +HnZM:tp폥ɅB`5$u- ;-E(ow"qY $g6 BIծ];~͵~zuK/e<߅VZ j~kk[bK\ V׏El1]`]b6 VVݻwT({wFy>Z=d†7NM} Ga@92oPB#F(;8O#޵kWJ9o7n4iz( aI3ԭ[Wo#m8L x ..D(1DtCo6mӥ,hpl+ϴ/[K+.ްaHn޼YHN>]&iWSNU>Dkƌ&g}Vj ixb2PՃy>p^Z=* [Ӱa"eD*_x+{ꀃQ.]WX@`֭[P[nguzpyɃ!}Eҷo_UܹS}'zi&G~P̮ԩV(aĉzd dO0t CIt^[sqҳpl+$Ǘ L8EJzJ0l̻l@^7+,ݻwW{/+:ux{He\wAG':9 NVe`r75,y"?%\'kǏW}f~a l"Ǐ=X-~ÇW&LP6XQQA!L׭[:Q,肬!tñ\c_3 ˥!-T#@1Ȱ2٣Pq_ۧNp-_"Z 4Ы!:yքWXBSb?(\TX$@~xMKu߰EO{9 ݨQTP03Xz쩷!Bt'pl+kl#H>Ӣ\W]R}!9;lw ,Y͛?زׯ;_ foVX5qwAqٲez⊂kٲ0q%󪫮x[  |2|iø+r+J:T;^S8Vz@E`1V/a3Y$p҃ߖ48ŵ|$i_qrll"6lXSۘ[@ ldլYr:SK(<,Wt8]A0ipNjժ9m~4:>^0Ӡsz?bt~ "Sg.峁֊SxQO;v~Tމ[رcu[ʉ+82Dox}I!v۵k',3&3P;wV-RȶmԜ9ste;ujK }zAl…,E-p)_X4\rS Żk ]$:ʕ+sP8RESyo +-=WD? o7I䟓:Lo\lƼ`RB$ W'S`.$TYDH N]>FHO RC q   H  H=W#*bx6JܯUsvrLN'fSۄ*^FǨK~\I")$/XG!veqrwy~JLk*.+)5ka-ŐX$ cf,n˶ҢE#ڵkU>}T9ū\ttr7QJB0)x[z!;wZ`AtAH(#)˝Y$;v~m0P-!:|rYAwx Ł. c'ģQFo.N ˧h"ٺuk5yj! D(nqƍz^Yqꩧٳgkk%j,OJydꩧH&DCbwQm#+~ų9?"JLBG1_`>\SLEkݻjH~t\>(!D1-.?;C?DI4Vl>͛7!ظ_ڵ3hׯWGwUVaÆZ>ǼÖ8ps?kBv?qZicO= ؟BTħ.YgU%&ě-lwA?s=V*֥ 7H4%=L~E?c_as^gZWPs+ 6M6-㇐DvZg6m۶: 9W;š0@G=!^gРAj֬YzB(3X=zZ7{/BJwEwŊڤ3޽{Hk+./C&lxWPpF@ ȑ#3!/m1bJرcz~j[$0r?]v+7IG}'?w^zI1DtCo6mӥ,hpl+ϴ/[K+.ްa$jRڵӁaԩj͚5::̘1C뮻2ށg3w+%?XA7 AgWQta%[l?Q&[x>uy>͛7nVyOzѭ[7nݪ?: TR>Q")Fwܩ>޳gO3?~<1*f||rO8XO4)3leE?c˟K]?}?%H>Ӣl+iV+'&Mc*٘wbOu v]z8wGA{fG2Ai.;x~V#,;YaF԰Udspy?dkkFgLxPsݺu/$Va~TPq|,Y?P֭ĦM#YG![~ ǖrяAOgKCZ+.GbaebڰB̘g%y߾}ɓϧw-[>Fqt/}6$Jl/ \v 4PK.իx qu:t'$ xk#{5 B{G&s0qamгgOurjTtñ\c Lr_q(wJA`.^d7o`_~ ~5Vƪs ˖-W\˖-՝wY忁ZY%sUWe<-kY[>{?aU4a\9ٱDyСC5ukkrdÈm4(!E>Qs& AJ J!.>I#|cEӠr_6YNi3p>`2QL|v]["!V\(_X rsE |D9YA;PXf V!%DPpPxƌVq]>?r3 IA"VU \[ ˧ܨQIoA((B")  B&  B,R}dJ:^R(7k.\}dXmo(Bi~4r$ +6il|ERH.u-&_|CR!y쭷R^zi{7S忂 dRlX _IѬY3}k1(|"IX3ӧ񹰓DƲ 3駟kת>}rWl5_Js`KrI!-̝;W-X v $MQHr'uɎ;~[h" ԡCm v+vuuש˗Btr;~A@Dt8p`7;5R7|wq|HF,[V'O"PA4lqF=`y8Sٳm5n'f Gy:7]vFmȑ#:|.jJ5lPGCXCXw'ngrM:'N+m ?wŊs6#[!?i/;묳ԄxM.c>°ժU]ź?[%=L~E?c_as^gZWPs+ 6M6-㇐DvZg+8o۶HĕΜ93 QOHg<4h5k`2 /V*GꫯVƍŋ}Z (0"yAջw|Qػry>B=d†7NM} Ga@92oPB#F(Xm2y?}G] ĕEƍ;y;wTӟ减>HO x ~;^zI1DtCo6mӥ,hpl+ϴ/[K+.ްa$jRڵӁaԩj͚5::̘1C뮻2ށg3w+%?XA7 AgWQ <lٲED)_lA7oެ?s[=Ent|_ۺu .PI~CXD?G~'zgϞ*9F۷96",a'NV&M [cY`Iñ$i_W\RwzĉU&M1l̻l@^'ۺVVXOw^V=u~!ݛ@/ |YqtOc0dU9 &w[QV']!} 09~xu$O*ҏ _|?ui&5}ȇyѣ!tñ\c_3 ˥!-T#@1Ȱ21mX f懊3xJɓϧw-[>0iҽ|gln0`+| Z(pEQ?& 6ehРZt^w<[o߮ԡC>&tPkN5jJzXyma gϞz(5k:-8鐭E?c_ \gAҿP a\d5o<V-~x VX5qwAqٲez⊂a;Ď-ꪫ2ɽA^ϟ}<`B>a0sEfv(Q?tPxMZX5rdȀm4(!E>QsئPLPZxd[⤇>[cEMd9Ca1/l.X1By&B3fV0[]lIbvL*,L$A [Ua'LmPL.ҿrFM$QpAPC q  B ſL$AAXRu@1xuNPn*9 #$\)fr*j~d(u5i$Ե,c D,Bpk1ub#Ŭ_$wu~JLM9YfO\gM'HǬ ;>j,ۚJ-O?Uk׮U}Qrj J.HXB])$dS!,kh W`j'˶Tb1ls:.IDB\''u[;vԓGbbrW` aڴiC-_\+ra_ X/J"ǀR%V.!XҗLR>zh5Y~Zf-]wӨQ#u73gJbG> OO&[V'O"PAĞ7nܨ4^fϞlq<1+ <#zkUSO=(% Gj ]ų9"J tԡC?7:}$&wڗUҥs\GX 8o۶HĕW$ ЖL1? Rf,\FJwC]}Z /Btgk%ɻ<`ս{HwFy>B=d†7d+(O8y ȑ#3AC^HO BTtK3[!L?M6iR|J@4H҃S_8 ~X\g'[KMnk{Æ "yfm&>}=VM7Sf|9_3fdX X}Y=70Gp!@}v0G(0NիW^EnJ -[x R;X1!ZAS2Xb֍nݺe uuV|#\pJ <'JQD}Uw :}ǫ1U={tM,r+@:u' (,b˧|@~疺I&/$ %=| ǖrAOrgO/"?qIdzJ c}F2ah`œm]+, ݻwW{/+VR|}f޽{3ң@4El|o~@[N 6mҋبx[tA[:dOW./I.FS O\Ry$+!V0@ ^>s: _/4[>2jҽn0`+|(Z(pEQ?&L*l4h@Xna)!O:tlaSJ (d|̊su2o<ퟃz@޲S7<5v7r5j(47Lla +$={ꭥÇ(pHUt&±\_ODQF/$ǿs%K m~߂n0Ǫ_ ˖-W`˖-՝wY?)s=2߼ꪫ2ɹzE᳗&(XŐOsƵ|."+\Dy>8@S\aYueń"H rܕ' qۄ"fޮ];H/Bu7zKƯ-ʈg7|hSf8 wK`lo>Vz~@?HY־ enoߍ4萅1cd޹sgh"Ul۶M͙3G=۩S[z~@v3 wi2Q߮`g)2`[ W./|[?lt 63VK; iHH?tV0B2p r#>AVl[zr!+IL,av :'c "b)!+N[(7 f+8PLJ]>r3y޻$2A3le B1)uS\jD2_qAO H    T#YpuB"(i\>q=c܃&d}T9ŭ\֚tt{=nYOB縔\$\eD(Emdܹ BDŐb?/i꟩رnHbjrW` aڴiC-_Z`DxѣG":՚5kt?SLɫ1]wӨQ#u73gJbR ,iHnZMǼÖm"aG7N"O>d]tիWtI'v,mSڏ;묳܄}x9O=ܣjժ]p#&_ñ//䳦迠M>s`I lڴi?4:"= eTm۶͙\~ڒI:۽ϠAԬY QFmR=zPW_}<ŋ]Q`$)&mʨ{b('(Lp7K} Gr#LF7 *B\#F(;ECP'۰oXzX4ij=8a&7n\uGdsHJ>?#= MPY`~̏WڇmڴNϗ:sTׯAږ$_ñk? L>kϸH ErGrX5逘 ~5uT5p#5X3fȰ6=zUb`eGcnzaO N:WV?mݦ*+&B[lQ<@EwbC>kuOp0"0↰tRCn2ߺu~>֋ .@%Ma%t}87,&_@:us$aĉz$峘!)> N'N?O4IŏM~\1`5'=~ ǖrѯAOgM$OW^а$> ٜ-2kd[ hʄSW|}Ve@C޽{3ң@PAi.;x~V#y'"%c`+j+D~K?ORa߰>|X)rcUW;|p5a„OR>X?ywpaݺuZoڴI/&YlY3lMEc_נ'mYϥ!F*d ~g/f懎ey߾}ɓGwX :54(>"fkX# E+rXaEym4h@>a O:tlaS(lUԍ.X¨[-4$Is K[g? T%Kyi lٚ VM]`P[lZl}%~XIG߼ꪫ2@2%̟?_aC>a0qEW`Cjiꀫ\ +Z HhcmvW&B6`߰SoѦ(Ld?^Ȗ |f*? '&P yG۷o+=}g??=,dt-=ѯ_mK~\g6,wp?P$bذaNmc`- cnfΪ8UjCaað= p[PVz9h:Q񂹚O:%A ? 1@RkE)(ϧLt;vh?D-+rرlb%h{$NC [e$okB`߰t[i]viK?~F˜1c+ɬ(Ν;ERd۶mjΜ9޲:Ib[prK&>kA*l6Dc_ \?\䳜_ӆM>s63VKX9`xʕy\o97W0^n}d%W!lY/IDN 2CJD< !}D_V0[UlIbR3/P& )аtA(&5]>^-  #[ۂ  BA  T#YpAgF)X t~ؤY8MbP*'Qr5iүLMNm:I֓X$Rbr=>s{Ǎ@WLܿ ԏR͚5hX$ =df΅\5mMEUG(kת>}ryl5"C{:؂ Dr鴹L 0s7QJB0)h[z!;wn邐4QG3W%MOvǎuC V EzB-_*(}I3 J(,GQ#Sv駫5kh ~Ljm$u]4jH|j̙Q>l"ٺuk5yj! C(nqƍz!`SOUgJ݉yiGyDo'󩧞Hb ѣGp`22ZDIȥCi+oy`!P&wW!ҥs\߅VZ j~kk[s?['P~0]jekN.H>|X HǍCħ;z =G۱`M!j?SΪg͖6>"?xW0-iׯϥx#ʟ 6̅M$)iӦe8.XS&M_CI Sٸq㪿[ު!zCG}F MP{e||c a>ԯ__DA-=HDc_9װϵ@\,g+\r?ank{Æ "I#9}t{t@LN߿:u8p`iH_3fdX XVVvT<1 )\ook(#W%(6P^'N?It^ Pbnz fWruIO裆' >a3B>} 62ÀZl%5[E}?)|g+?aG^zzuAð@'4mh`œm]+V(~ݻE:uwgU4ݻ7#= R(gŭ?])"+l-`g~cU;|p5aecUOTTPF~Jܕ|_ <- k-ɗpl+ƧBKD #H`?Kسg 3fsC2xo>uɌ wh:54(>"fkX# E+rX!Eym4h@\>a O:tlaSHge*iϞ=`+ W_[7x2[4?xoި[ӷeƒF7l1 I]Hs)\O.Bro0GS!k,Y͛7زׯfgVX5quAmٲez⊂jٲu0q`%󪫮xft|,pe[V~2ȧ9 Z>&ViQ?tP8MpՁcNJ· m4tL=>QM._/B>i_ , .c+T dÁ8=– ̤ Aao>Vz!L~x& ,[z_"[&G??ʧ i*KɅM$QÆ vjs|X cOG@aað= A(LVx9 Ns5tNK\] /ԗKll);(Zcw}>ex@[;V5]YĘYmﯟi_ I>&1~:( Vm4/w׮]z?93fpΝբET)m65g]/N}l`ig-(Z-݆pl+KGvkm3Ni3p>`N?[ !W\ׅ%(ˍ|xYm"Uȅ|gD ȉqP0QsH ('#S`.ŤgRg^ L$AR[a'pmPLj|5j"?*P(]~X$AAXDRAAELn2s),r{nv%R{uटD١^LNQ8YOٸIB,wD+lA(ʵ~sYfElc%XB$IENjčY{j?')z!4,|D] KB܅)p[z!;wZ`AtA?A( fr 5ԢE U.0$;v'ܴkQ;w7s 䑘ܥx*!y5jnf5sX P8 6lݺG~zQvM4h5kjذ.=Zv.2/_g{M/^A$WXWX'<[nQݻwJw)_|3_N'ի? Er˖-z|A|] ࡇ4YftRg0ֵnݺeM\v[e__~n,r,W\]r?AȅHz}Qt4i;Xݺu 7nO-b2U[b4@ofXMԩaP6G?~\᠑UU^z6qD]I5x&QaKUa>ϴ/پFq[N6mғ([FhCbl[_=& wތ(P4ɗ-qtW`qBX̶!|?-Jɯ;8 B.=ac|7jP3٫D`}Q`ٖau̙'"(*6h1jpjLGɓ>WtiwVaIJгgO54|p1uƀ'mٳGˀ](-SPžow`-7(Ha!X6\$_ŭ?,jǎS64hA-w`mu47A65n~\_]!Brz-uf]lX1lRy Gp,Q;) χDf"9d{wzK1aeUT ^r% sl7fS uOm' KVY+N֙t ;l3qER ls}>[w<`bmml4ls_? l-#Xdq\O`Iɰaê66W>t8Mg[j*'zxr}cǎe3މ~Kk J EiE8;V3[u>?(4' a- &T_(Ra۶mK]ұ0`ŀ,g3]prK&>GA'Em67φMN|P h#XH LPT7kNQ-.;O?wD7#vCwߟ+.O~s_agB"ʻ̵(wf/}j ͅ4u3lBŅ @򏒞+. 9ЕD#D"ytn*%QsH (.VqqA N R|$2 (vPDR`+*-]⹔zH?A(2ʆr_m5A`-,fA(d")  B&  B,ɚ ~"u>R./\{SmVcU7֡AZPI?6xfMB,B4.ZK8X קBp$wT˛p?jʕJ!ZHEH 1 X{Z|Vt{ȺrҳM"]u/2\DrBbia.˯@^}&Ao/<`\>^G)\,X v $pLh@"po!0ڍ6mڨ;$LB1iɵ7.BۻP-?/w"h/ (*6mBBV/Zh!ei*H'}!mZY-HJ1%1Oe|}9뽏HO<3g9;ᚒ R^;ضm[wM̞=; O'+L0!%wVR?,iꫯfm@c)zAK$Es9k֬bɒ%"K+s`~ze WVG`e2b{O\hѢvu٢.[fMXӃ>8,KѓQwK˒s)kt֭[ÿ@!2oY^`cg1*uօVYa꿩;?ҟc ,9H_Ν+K{gR5Ys`e /8̵aKV`mR,gmcCw׏ITW^HǏǸq0ϱ,>bܸqk .7zb'k8͛þ=Нpowz&ߤs I[n$t?c#P|Xc;%u+d{1v~꿶u/smemS\#H zvÒV9ãuXOzҤI§ۙan-iñ"_uE].b٢.S|'aNguVd+3L81c_P2!&뮻߳F_~Z% :e1,(Z!v~V^]\qy׳)T0T=S/MVp 8^ع9@3\[ou˟)׷MNI&s9}\+΃f}Bw|pK@o:7R9s愛y5\dÚtIŋHVGj5BXUg}BɐMXw~꿶?VxDmS9mzXFVeЫzܞ|bժUIߥ5DP+syPQ9r?v[LC0 .`~7 M/G,}ӞnE;h_C/T/{g?V;;3T DV\9[Jg)C=̓xbol{g|p!0mǺkQW:^ct_}աs9AOZ!dPUQ8M-?0|.]ϟ&}8a"z栗9sfa̭C נ>H җK= TH!Jp ԩS(?8w+ӧOC=̳\ )闒?o݋I\ן~y :r8K.)?uczcɥ^pc.HMo4տI==uym'iݤ柺տbG= Iv}Glm1#HQnAOmZKt瓠tO⋃*+2 25ܘx8|*E1-2 {Ep]D8?2+`2F됕igRp_]2$r -4A뎴iF'On9`ռMQ0<\!γKYw}1TKz;У<7P7⚈?1NyE?2I}@i/=N{(Vƶ,iB6eֶ|SŜnOǶǤ䟺meȝkXOUJ\L'ڐn?ױw $⮜CF wq3aE^}$x0\bXDs5bi?t hңU=Cc(nm0܍Z^Zvx}CuOǶKmHF$FO?g~~@R$IY $%Iezd'^* _u_|S *_)ºϣ Ak*Ocmf$/]Arn΍W#Q~C;4z3^dg7~1F?o{~x`k,Q2eLHfΗَkS,[T)/3ZX⭮>xA3o?ÚgfHL+Y38#Y[,yF?,G~m%o-5цG͈Hs1SO=5hD:`k@75/Y5'KAɒHgw.DzAz#HpjK,)mzY?QGXӒES,'|R ckV|ƍwyg㏝,:nZۼ0R,vw[n61~o :*~#gΜPȘC%y5}Х%b~qe'N;;iҤbܸqae"9# s=ES9VĪlgR6iz_ s*@|Ɵwyۙ\tSp GN)7ܨt'뮻2q. 8S7m/]Avi&gp{;Ǽz+׿_o[~wmK+Vvqա{xߪXȵ=cm2hu~$- ЫIŪ,\~g-&LSitk+sL>裁r`~ Ӻ+_bT0Jx≰PdZw{nIJeB$s$_z饤+1'_y啁bcK,fOCK+W?|}&m8Ɨ_~9f~Í_jU8~Z|Kuן9UW'|2K'n4_~yߠŋ_o[~oJ *1oNI)9~*?{\ ɰB ;VTd2H Z{w=zNj<0Jե8~z:/:/@Fq)$_ p})14e?7KW_})Mb_>`ޖnJMC]U~S$=qԹ@{WV46{$׬YK!cZ0kVZn]豩D!~o1p oSqb~MMJy/|v=D)Ap2s< 64c@)5bxj}zcdG-Ã'#<2.'_uU;Ub鷫qOl'7q8zSht{؆}vmo+Y~ۗߡoj`(ck km{6b$o6CprлBTO"] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Threshold-based Reproducible Builds pluggable transport using your trusted rebuilders" readme = "README.md" license = "Apache-2.0 OR MIT-0" repository = "https://github.com/kpcyrd/repro-threshold" [[bin]] name = "repro-threshold" path = "src/main.rs" [dependencies.anyhow] version = "1" [dependencies.astral-tokio-tar] version = "0.6" [dependencies.async-compression] version = "0.4" features = [ "tokio", "xz", ] [dependencies.bytes] version = "1" [dependencies.clap] version = "4.5" features = ["derive"] [dependencies.clap_complete] version = "4.5" [dependencies.crossterm] version = "0.29" features = ["event-stream"] [dependencies.data-encoding] version = "2" [dependencies.deb822-fast] version = "0.2.0" [dependencies.env_logger] version = "0.11" [dependencies.futures] version = "0.3" [dependencies.in-toto] version = "0.4" [dependencies.log] version = "0.4" [dependencies.pem] version = "3" [dependencies.ratatui] version = "0.30" [dependencies.reqwest] version = "0.13" features = [ "json", "rustls", ] default-features = false [dependencies.serde] version = "1" features = ["derive"] [dependencies.serde_json] version = "1" [dependencies.sha2] version = "0.11" [dependencies.tokio] version = "1.48" features = [ "fs", "io-std", "macros", "rt-multi-thread", ] [dependencies.tokio-ar] version = "0.9.0" [dependencies.toml] version = "1" [dependencies.url] version = "2" features = ["serde"] repro-threshold-0.1.2/Cargo.toml.orig000064400000000000000000000020031046102023000156270ustar 00000000000000[package] name = "repro-threshold" version = "0.1.2" description = "Threshold-based Reproducible Builds pluggable transport using your trusted rebuilders" authors = ["kpcyrd "] license = "Apache-2.0 OR MIT-0" repository = "https://github.com/kpcyrd/repro-threshold" edition = "2024" [dependencies] anyhow = "1" astral-tokio-tar = "0.6" async-compression = { version = "0.4", features = ["tokio", "xz"] } bytes = "1" clap = { version = "4.5", features = ["derive"] } clap_complete = "4.5" crossterm = { version = "0.29", features = ["event-stream"] } data-encoding = "2" deb822-fast = "0.2.0" env_logger = "0.11" futures = "0.3" in-toto = "0.4" log = "0.4" pem = "3" ratatui = "0.30" reqwest = { version = "0.13", default-features = false, features = ["json", "rustls"] } serde = { version = "1", features = ["derive"] } serde_json = "1" sha2 = "0.11" tokio = { version = "1.48", features = ["fs", "io-std", "macros", "rt-multi-thread"] } tokio-ar = "0.9.0" toml = "1" url = { version = "2", features = ["serde"] } repro-threshold-0.1.2/LICENSE-Apache-2.0000064400000000000000000000227731046102023000154410ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS repro-threshold-0.1.2/LICENSE-MIT-0000064400000000000000000000016261046102023000145430ustar 00000000000000MIT No Attribution Copyright (c) 2025 kpcyrd Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. repro-threshold-0.1.2/README.md000064400000000000000000000064601046102023000142320ustar 00000000000000# repro-threshold Threshold-based Reproducible Builds pluggable transport using your trusted rebuilders. ![](.github/assets/screenshot-home.png) Run `repro-threshold` with no arguments to start a [`ratatui`](https://github.com/ratatui/ratatui) based configuration interface. **Status**: Very experimental ## Why this exists - Open Source gives you the source code and often also provides prebuilt binaries - You have to trust the build server (and their operators) that they've actually used this source code with no modifications - There is an [ongoing effort](https://reproducible-builds.org/) to make the build deterministic and the build environment documented so other people can reproduce bit-by-bit identical binaries from source code - With `repro-threshold` you can enforce a policy to only install packages reproduced by multiple groups you trust ## Who to trust? ![](.github/assets/screenshot-rebuilders.png) Who you trust to do this verification is a very personal choice and there's no obvious right or wrong. This is why `repro-threshold` let's you configure this yourself, along with the number of required groups having confirmed the binary. There's a [public list of groups](https://github.com/kpcyrd/rebuilderd-community), this can automatically be loaded by pressing `ctrl+R` in the rebuilder selection screen of the TUI. The trust necessary to the individual rebuilder is limited, most importantly: > Out of the rebuilders you select, > > and the threshold configured, > > you trust no group is going to collude > > big enough to exceed your threshold. If necessary, you can also always run your own. The security control by `repro-threshold` is additive, this means even if it gets fully bypassed/broken somehow, you won't be worse off than without it. ## What is the 'blindly trust' set? As of this writing, it's currently not practical/possible to build a Debian/Arch Linux computer with reproducible-only packages, so there's a mechanism to exclude packages from this check and permit installation even if there's no evidence it was built from the given source code. ## What this doesn't fix The Reproducible Builds stack gives you a trusted path from source code to binary. It doesn't help if the source code itself is malicious/harmful. The source code may still contain security vulnerabilities or [intentional backdoors](https://en.wikipedia.org/wiki/XZ_Utils_backdoor). Choose wisely what software you put into your computer. ## Privacy notes The rebuilders you configure as trusted can see the packages and updates you are interested in. ## Integration: alpm ⚠️ This hasn't been implemented yet ``` # /etc/pacman.conf XferCommand=/usr/bin/repro-threshold transport alpm -O %o %u ``` ## Integration: apt Register repro-threshold as an available apt transport method: ``` ln -s /usr/bin/repro-threshold /usr/lib/apt/methods/reproduced+http ln -s /usr/bin/repro-threshold /usr/lib/apt/methods/reproduced+https ``` Update your sources in /etc/apt/ to use this transport method: ``` #deb [arch=amd64] reproduced+http://deb.debian.org/debian unstable main Types: deb URIs: reproduced+http://deb.debian.org/debian Suites: stable stable-updates Components: main Architectures: amd64 Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg ``` ![](.github/assets/screenshot-apt.png) ## License `Apache-2.0 OR MIT-0` repro-threshold-0.1.2/src/app.rs000064400000000000000000000201521046102023000146620ustar 00000000000000use crate::config::Config; use crate::errors::*; use crate::event::Event; use crate::http; use crate::rebuilder::{self, Rebuilder, Selectable}; use crossterm::event::EventStream; use ratatui::{DefaultTerminal, widgets::ListState}; use std::iter; use tokio::task::JoinSet; #[derive(Debug)] pub enum View { Home, Rebuilders { scroll: ListState }, BlindlyTrust { scroll: ListState }, } impl View { pub const fn home() -> Self { View::Home } pub fn rebuilders() -> Self { let mut scroll = ListState::default(); scroll.select_first(); View::Rebuilders { scroll } } pub fn blindly_trust() -> Self { let mut scroll = ListState::default(); scroll.select_first(); View::BlindlyTrust { scroll } } } #[derive(Debug)] pub struct App { pub view: Option, // Keep this state even when switching views pub home_scroll: ListState, pub confirm: bool, pub config: Config, pub rebuilders: Vec>, } impl App { pub fn new(config: Config) -> Self { let mut home_scroll = ListState::default(); home_scroll.select_first(); let mut app = Self { view: Some(View::home()), home_scroll, confirm: false, config, rebuilders: vec![], }; app.rebuilders = app.config.resolve_rebuilder_view(); app } pub fn scroll(&mut self) -> &mut ListState { match &mut self.view { Some(View::Rebuilders { scroll }) => scroll, Some(View::BlindlyTrust { scroll }) => scroll, _ => &mut self.home_scroll, } } pub async fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { let mut events = EventStream::new(); while self.view.is_some() { terminal.draw(|frame| { frame.render_widget(&mut self, frame.area()); })?; match Event::read(&mut events).await { #[allow( clippy::collapsible_match, reason = "https://github.com/rust-lang/rust-clippy/issues/17033" )] Some(Event::Yes) => { if self.confirm { // handle yes action self.confirm = false; } } Some(Event::No) => { /* if self.confirm { // handle no action self.confirm = false; } */ // TODO: dummy code, open the prompt self.confirm = true; } Some(Event::ScrollUp) => { self.scroll().select_previous(); } Some(Event::ScrollDown) => { self.scroll().select_next(); } Some(Event::ScrollFirst) => { self.scroll().select_first(); } Some(Event::ScrollLast) => { self.scroll().select_last(); } Some(Event::Reload) => { if let Some(View::Rebuilders { .. }) = self.view { let http = http::client(); let list = rebuilder::fetch_rebuilderd_community(&http).await?; self.config.cached_rebuilderd_community = list; self.config.save().await?; let mut tasks = JoinSet::new(); for rebuilder in self .config .custom_rebuilders .iter() .chain(&self.config.cached_rebuilderd_community) { let http = http.clone(); let url = rebuilder.url.clone(); tasks.spawn(async move { let keyring = http.fetch_signing_keyring(&url).await; (url, keyring) }); } while let Some((url, keyring)) = tasks.join_next().await.transpose()? { let keyring = match keyring { Ok(keyring) => keyring, Err(_err) => { // Can't render errors in TUI apps like this // warn!("Failed to fetch signing keyring for {}: {:#}", url, err); continue; } }; for rebuilder in iter::empty() .chain(&mut self.config.custom_rebuilders) .chain(&mut self.config.cached_rebuilderd_community) .chain(&mut self.config.trusted_rebuilders) .filter(|r| r.url == url) { rebuilder.signing_keyring = keyring.clone(); } } self.config.save().await?; self.rebuilders = self.config.resolve_rebuilder_view(); } } Some(Event::Toggle) => { if let Some(View::Rebuilders { scroll }) = self.view && let Some(idx) = scroll.selected() && let Some(rebuilder) = self.rebuilders.get_mut(idx) { if rebuilder.active { self.config .trusted_rebuilders .retain(|r| r.url != rebuilder.item.url); } else { self.config.trusted_rebuilders.push(rebuilder.item.clone()); } self.config.save().await?; rebuilder.active = !rebuilder.active; } } Some(Event::Enter) => { if let Some(View::Home) = self.view { match self.home_scroll.selected() { Some(0) => (), Some(1) => { self.view = Some(View::rebuilders()); self.rebuilders = self.config.resolve_rebuilder_view(); } Some(2) => { self.view = Some(View::blindly_trust()); } Some(3) => self.view = None, _ => {} } } } Some(Event::Plus) => { if let Some(View::Home) = self.view && self.home_scroll.selected() == Some(0) { let threshold = &mut self.config.rules.required_threshold; *threshold = threshold.saturating_add(1); self.config.save().await?; } } Some(Event::Minus) => { if let Some(View::Home) = self.view && self.home_scroll.selected() == Some(0) { let threshold = &mut self.config.rules.required_threshold; *threshold = threshold.saturating_sub(1); self.config.save().await?; } } Some(Event::Esc) => { self.view = Some(View::home()); } Some(Event::Quit) => { self.view = if let Some(View::Home) = self.view { None } else { Some(View::home()) } } None => {} } } Ok(()) } } repro-threshold-0.1.2/src/args.rs000064400000000000000000000071011046102023000150350ustar 00000000000000use clap::{ArgAction, CommandFactory, Parser}; use clap_complete::Shell; use std::io::stdout; use std::path::PathBuf; use url::Url; #[derive(Debug, Parser)] #[command(version)] pub struct Args { /// Increase logging output (can be used multiple times) #[arg(short, long, global = true, action(ArgAction::Count))] pub verbose: u8, #[clap(subcommand)] pub subcommand: Option, } #[derive(Debug, Parser)] pub enum SubCommand { #[clap(subcommand)] Transport(Transport), #[clap(subcommand)] Plumbing(Plumbing), } /// Integrations for package managers #[derive(Debug, Parser)] pub enum Transport { /// Integrations for Pacman's XferCommand= option Alpm { /// The output file path #[arg(short = 'O', long)] output: PathBuf, /// The package to download url: Url, #[command(flatten)] options: TransportOptions, }, /// Integrations for APT's transport methods Apt, } #[derive(Debug, Parser)] pub struct TransportOptions { /* /// Example: socks5://127.0.0.1:9050 #[arg(long)] pub proxy: Option, /// Only use the proxy for transparency signatures, not the pkg #[arg(long)] pub bypass_proxy_for_pkgs: bool, */ /// Use these rebuilders instead of the configured ones #[arg(long = "rebuilder")] pub rebuilders: Vec, /// Number of required confirms to accept a package as reproduced #[arg(long)] pub required_confirms: Option, /// Blindly trust these packages, even if nobody could reproduce the binary #[arg(long)] pub blindly_trust: Vec, } /// Low-level commands and utilities #[derive(Debug, Parser)] pub enum Plumbing { /// Fetch a curated list of well-known rebuilders FetchRebuilderdCommunity, /// Add a new rebuilder as trusted AddRebuilder { /// The rebuilder URL url: Url, /// Set a human-friendly name for the rebuilder (defaults to the URL domain) #[arg(long = "name")] name: Option, }, /// Remove a rebuilder from the trusted set RemoveRebuilder { /// The rebuilder URL url: Url, }, /// List configured rebuilders ListRebuilders { /// Show all known rebuilders, not just active/trusted ones #[arg(short = 'a', long = "all")] all: bool, }, /// Add a package to blindly-trust set AddBlindlyTrust { /// Package name pkg: String, }, /// Remove a package from blindly-trust set RemoveBlindlyTrust { /// Package name pkg: String, }, /// List packages in blindly-trust set ListBlindlyTrust, /// Authenticate a package through rebuilder attestations Verify { #[arg(short = 'S', long = "signing-key")] signing_keys: Vec, #[arg(short = 'A', long = "attestation")] attestations: Vec, #[arg(short = 'R', long = "rebuilder")] rebuilders: Vec, #[arg(short = 't', long = "threshold")] threshold: usize, /// The file to authenticate file: PathBuf, }, /// Parse metadata from a .deb file InspectDeb { /// The .deb file to inspect file: PathBuf, }, Completions(Completions), } /// Generate shell completions #[derive(Debug, Parser)] pub struct Completions { pub shell: Shell, } impl Completions { pub fn generate(&self) { clap_complete::generate( self.shell, &mut Args::command(), env!("CARGO_PKG_NAME"), &mut stdout(), ); } } repro-threshold-0.1.2/src/attestation.rs000064400000000000000000000200371046102023000164430ustar 00000000000000use crate::errors::*; use crate::http; use crate::inspect::deb::Deb; use in_toto::{ crypto::{HashAlgorithm, KeyId, PublicKey}, models::{Metablock, MetadataWrapper}, }; use sha2::{Digest, Sha256}; use std::collections::{BTreeMap, BTreeSet}; use std::path::Path; use std::slice; use std::sync::Arc; use tokio::io::{AsyncRead, AsyncReadExt}; use tokio::{fs, task::JoinSet}; use url::Url; pub async fn sha256_file(mut reader: R) -> Result> { let mut hasher = Sha256::new(); let mut buffer = [0u8; 8192]; loop { let n = reader.read(&mut buffer).await?; if n == 0 { break; } hasher.update(&buffer[..n]); } Ok(hasher.finalize().to_vec()) } pub struct Attestation { metablock: Metablock, } impl Attestation { pub fn parse(bytes: &[u8]) -> Result { let metablock: Metablock = serde_json::from_slice(bytes)?; Ok(Attestation { metablock }) } pub async fn parse_file(path: &Path) -> Result { let attestation = fs::read(path).await?; Self::parse(&attestation) } #[cfg(test)] pub async fn verify( &self, reader: R, public_key: &PublicKey, ) -> Result<()> { let sha256 = sha256_file(reader).await?; self.verify_sha256(&sha256, public_key) } pub fn verify_sha256(&self, sha256: &[u8], public_key: &PublicKey) -> Result<()> { let MetadataWrapper::Link(link) = &self.metablock.metadata else { bail!("Attestation metadata is not an in-toto Link") }; // check signature (to avoid a warning, remove all other signatures) let mut metablock = self.metablock.clone(); metablock .signatures .retain(|sig| sig.key_id() == public_key.key_id()); metablock .verify(1, slice::from_ref(public_key)) .context("Failed to verify attestation signature")?; // verify file is one of the products for hashes in link.products.values() { let Some(expected) = hashes.get(&HashAlgorithm::Sha256) else { continue; }; if expected.value() == sha256 { return Ok(()); } } bail!("SHA256 hash does not match any product hash in attestation"); } pub fn list_key_ids(&self) -> Vec { self.metablock .signatures .iter() .map(|sig| sig.key_id().to_owned()) .collect() } } #[derive(Default)] pub struct Tree { map: BTreeMap>>, } impl Tree { pub fn insert(&mut self, label: String, attestation: Attestation) { let item = Arc::new((label, attestation)); let attestation = &item.as_ref().1; for key_id in attestation.list_key_ids() { self.map.entry(key_id).or_default().push(Arc::clone(&item)); } } pub fn merge(&mut self, other: Tree) { for (key_id, attestations) in other.map { self.map.entry(key_id).or_default().extend(attestations); } } pub fn get(&self, key_id: &KeyId) -> Option<&[Arc<(String, Attestation)>]> { self.map.get(key_id).map(|v| v.as_slice()) } pub fn verify<'a, I: IntoIterator>( &self, sha256: &[u8], signing_keys: I, ) -> BTreeSet { let mut confirms = BTreeSet::new(); for signing_key in signing_keys { let key_id = signing_key.key_id(); let Some(attestations) = self.get(key_id) else { continue; }; for attestation in attestations { let (attestation_path, attestation) = attestation.as_ref(); if attestation.verify_sha256(sha256, signing_key).is_ok() { debug!( "Successfully verified attestation {attestation_path:?} with signing key {key_id:?}" ); confirms.insert(key_id.to_owned()); // We only count one vote per key, so skip the other attestations and continue with the next key break; } else { debug!( "Failed to verify attestation {attestation_path:?} with signing key {key_id:?}" ); } } } confirms } } pub async fn fetch_remote>( http: &http::Client, rebuilders: I, inspect: Deb, ) -> Tree { let mut tasks = JoinSet::new(); let inspect = Arc::new(inspect); for url in rebuilders { let http = http.clone(); let inspect = inspect.clone(); tasks.spawn(async move { http.fetch_attestations_for_pkg(&url, &inspect).await }); } let mut attestations = Tree::default(); while let Some(res) = tasks.join_next().await { match res { Ok(Ok(response)) => attestations.merge(response), Ok(Err(err)) => warn!("Failed to fetch remote attestations: {err:#}"), Err(err) => warn!("Rebuilder task panicked: {err:#}"), } } attestations } pub async fn load_all_attestations, P: AsRef>(paths: I) -> Tree { let mut tree = Tree::default(); for path in paths { let path = path.as_ref(); match Attestation::parse_file(path).await { Ok(attestation) => tree.insert(path.display().to_string(), attestation), Err(err) => { error!("Failed to read attestation {path:?}: {err:#}"); } } } tree } #[cfg(test)] mod tests { use super::*; use crate::signing; use tokio::fs::File; #[tokio::test] async fn test_hash_file() { let file = File::open("test_data/filesystem-2025.10.12-1-any.pkg.tar.zst") .await .unwrap(); let hashed = sha256_file(file).await.unwrap(); assert_eq!( data_encoding::HEXLOWER.encode(&hashed), "6b6c3fee7432204840d3b6afc9bc1a68c28f591a47fb220071715c40cca956df" ); } #[tokio::test] async fn test_verify_attestation_success() { let pem_data = include_bytes!("../test_data/reproducible-archlinux.pub"); let key = signing::pem_to_pubkeys(pem_data) .unwrap() .next() .unwrap() .unwrap(); let file = File::open("test_data/filesystem-2025.10.12-1-any.pkg.tar.zst") .await .unwrap(); let attestation = include_bytes!("../test_data/filesystem-2025.10.12-1-any.in-toto.link"); let attestation = Attestation::parse(attestation).unwrap(); attestation.verify(file, &key).await.unwrap(); } #[tokio::test] async fn test_verify_attestation_wrong_file() { let pem_data = include_bytes!("../test_data/reproducible-archlinux.pub"); let key = signing::pem_to_pubkeys(pem_data) .unwrap() .next() .unwrap() .unwrap(); let file = File::open("Cargo.toml").await.unwrap(); let attestation = include_bytes!("../test_data/filesystem-2025.10.12-1-any.in-toto.link"); let attestation = Attestation::parse(attestation).unwrap(); let result = attestation.verify(file, &key).await; assert!(result.is_err()); } #[tokio::test] async fn test_verify_attestation_invalid_signature() { let pem_data = include_bytes!("../test_data/reproducible-archlinux.pub"); let key = signing::pem_to_pubkeys(pem_data) .unwrap() .next() .unwrap() .unwrap(); let file = File::open("test_data/filesystem-2025.10.12-1-any.pkg.tar.zst") .await .unwrap(); let attestation = include_bytes!("../test_data/filesystem-2025.10.12-1-any.INVALID.in-toto.link"); let attestation = Attestation::parse(attestation).unwrap(); let result = attestation.verify(file, &key).await; assert!(result.is_err()); } } repro-threshold-0.1.2/src/config.rs000064400000000000000000000122541046102023000153530ustar 00000000000000use crate::{ errors::*, rebuilder::{Rebuilder, Selectable}, }; use serde::{Deserialize, Serialize}; use std::collections::{BTreeSet, HashSet}; use std::path::{Path, PathBuf}; use tokio::{fs, io}; const PATH: &str = "/etc/repro-threshold.conf"; #[derive(Debug, Default, Serialize, Deserialize)] pub struct Rules { /// Number of rebuilder attestations required until we believe them #[serde(default)] pub required_threshold: usize, /// Blindly allow these packages, even if nobody could reproduce the binary #[serde(default)] pub blindly_trust: BTreeSet, } #[derive(Debug, Default, Serialize, Deserialize)] pub struct Config { /// Rules for attestation policy #[serde(default)] pub rules: Rules, /// Rebuilders selected as trusted by the user #[serde( default, rename = "trusted_rebuilder", skip_serializing_if = "Vec::is_empty" )] pub trusted_rebuilders: Vec, /// Rebuilders added manually by the user #[serde( default, rename = "custom_rebuilder", skip_serializing_if = "Vec::is_empty" )] pub custom_rebuilders: Vec, /// Cached list of rebuilders from rebuilderd-community #[serde(default, skip_serializing_if = "Vec::is_empty")] pub cached_rebuilderd_community: Vec, } impl Config { fn new() -> Self { Default::default() } fn path_override() -> Option { std::env::var_os("REPRO_THRESHOLD_CONFIG").map(PathBuf::from) } fn path() -> PathBuf { Self::path_override().unwrap_or_else(|| PathBuf::from(PATH)) } async fn path_writable() -> Result { if let Some(path) = Self::path_override() { Ok(path) } else { match fs::read_link(PATH).await { Ok(path) => { if path.is_absolute() { Ok(path) } else { let parent = Path::new(PATH).parent() .with_context(|| format!("Failed to get parent directory of config path: {PATH:?}"))?; Ok(parent.join(path)) } }, Err(err) if err.kind() == io::ErrorKind::NotFound => { bail!("The system isn't setup for interactive configuration, symlink does not exist: {PATH:?}") }, Err(err) => Err(Error::from(err) .context(format!("Can't resolve symlink, system may not be setup for interactive configuration: {PATH:?}"))), } } } // XXX: these are provisory, replace with more robust implementation later async fn load_file(path: &Path) -> Result { let config = match fs::read_to_string(&path).await { Ok(content) => toml::from_str(&content) .with_context(|| format!("Failed to parse config file: {path:?}"))?, Err(err) if err.kind() == io::ErrorKind::NotFound => Config::new(), Err(err) => { return Err( Error::from(err).context(format!("Failed to read config file: {path:?}")) ); } }; Ok(config) } pub async fn load() -> Result { let path = Self::path(); Self::load_file(&path).await } pub async fn load_writable() -> Result { let path = Self::path_writable().await?; Self::load_file(&path).await } // XXX: these are provisory, replace with more robust implementation later pub async fn save(&self) -> Result<()> { let path = Self::path_writable().await?; if let Some(parent) = path.parent() { fs::create_dir_all(parent) .await .with_context(|| format!("Failed to create config directory: {parent:?}"))?; } let contents = toml::to_string_pretty(self)?; fs::write(&path, contents) .await .with_context(|| format!("Failed to write config file: {path:?}"))?; Ok(()) } fn rebuilders_by_precedence(&self) -> Vec> { let mut rebuilders = Vec::new(); rebuilders.extend(self.trusted_rebuilders.iter().map(|r| Selectable { active: true, item: r, })); rebuilders.extend(self.custom_rebuilders.iter().map(|r| Selectable { active: false, item: r, })); rebuilders.extend(self.cached_rebuilderd_community.iter().map(|r| Selectable { active: false, item: r, })); rebuilders } pub fn rebuilder_by_url(&self, url: &str) -> Option> { self.rebuilders_by_precedence() .into_iter() .find(|r| r.item.url.as_str() == url) } pub fn resolve_rebuilder_view(&self) -> Vec> { let mut deduplicate = HashSet::new(); let mut rebuilders = Vec::new(); for rebuilder in self.rebuilders_by_precedence() { if deduplicate.insert(rebuilder.item.url.as_str()) { rebuilders.push(rebuilder.into()); } } rebuilders } } repro-threshold-0.1.2/src/errors.rs000064400000000000000000000002061046102023000154140ustar 00000000000000pub use anyhow::{Context as _, Error, Result, anyhow, bail}; #[allow(unused_imports)] pub use log::{debug, error, info, trace, warn}; repro-threshold-0.1.2/src/event.rs000064400000000000000000000027241046102023000152300ustar 00000000000000use crossterm::event::{EventStream, KeyCode, KeyModifiers}; use futures::StreamExt; pub enum Event { Yes, No, ScrollUp, ScrollDown, ScrollFirst, ScrollLast, Reload, Toggle, Plus, Minus, Enter, Esc, Quit, } impl Event { pub async fn read(stream: &mut EventStream) -> Option { let event = stream.next().await?.ok()?.as_key_press_event()?; match event.code { KeyCode::Char('y') => Some(Event::Yes), KeyCode::Char('n') => Some(Event::No), KeyCode::Char('k') | KeyCode::Up => Some(Event::ScrollUp), KeyCode::Char('j') | KeyCode::Down => Some(Event::ScrollDown), KeyCode::Char('g') | KeyCode::Home => Some(Event::ScrollFirst), KeyCode::Char('G') | KeyCode::End => Some(Event::ScrollLast), KeyCode::Char('r') if event.modifiers.contains(KeyModifiers::CONTROL) => { Some(Event::Reload) } KeyCode::Char(' ') => Some(Event::Toggle), KeyCode::Char('+') | KeyCode::Right => Some(Event::Plus), KeyCode::Char('-') | KeyCode::Left => Some(Event::Minus), KeyCode::Enter => Some(Event::Enter), KeyCode::Esc => Some(Event::Esc), KeyCode::Char('q') => Some(Event::Quit), KeyCode::Char('c') if event.modifiers.contains(KeyModifiers::CONTROL) => { Some(Event::Quit) } _ => None, } } } repro-threshold-0.1.2/src/http.rs000064400000000000000000000113451046102023000150650ustar 00000000000000use crate::attestation::{self, Attestation}; use crate::errors::*; use crate::inspect::deb::Deb; use serde::Deserialize; use std::time::Duration; use url::Url; const USER_AGENT: &str = concat!( env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"), " (+", env!("CARGO_PKG_REPOSITORY"), ")", ); const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); const READ_TIMEOUT: Duration = Duration::from_secs(60); pub fn client() -> Client { let client = reqwest::Client::builder() .user_agent(USER_AGENT) .connect_timeout(CONNECT_TIMEOUT) .read_timeout(READ_TIMEOUT) .build() .expect("Failed to setup HTTP client"); Client { client } } #[derive(Clone)] pub struct Client { client: reqwest::Client, } impl Client { pub fn get(&self, url: U) -> reqwest::RequestBuilder { self.client.get(url) } pub async fn fetch_signing_keyring(&self, url: &Url) -> Result { let (mut url, base_url) = (url.clone(), url); url.path_segments_mut() .map_err(|_| anyhow!("Failed to get path from url: {base_url}"))? .pop_if_empty() .push("api") .push("v1") .push("meta") .push("public-keys"); debug!("Running search query on rebuilder: {url}"); let response = self .get(url.clone()) .send() .await .with_context(|| format!("Failed to fetch url: {url}"))? .error_for_status() .with_context(|| format!("Failed to fetch url: {url}"))? .json::() .await .with_context(|| format!("Failed to fetch url: {url}"))?; response .current .into_iter() .next() .with_context(|| format!("No public keys found at url: {url}")) } pub async fn fetch_attestations_for_pkg( &self, url: &Url, inspect: &Deb, ) -> Result { let (mut url, base_url) = (url.clone(), url); url.path_segments_mut() .map_err(|_| anyhow!("Failed to get path from url: {base_url}"))? .pop_if_empty() .push("api") .push("v1") .push("packages") .push("binary"); url.query_pairs_mut() .append_pair("name", &inspect.name) .append_pair("version", &inspect.version) .append_pair("architecture", &inspect.architecture); debug!("Running search query on rebuilder: {url}"); let search = self .get(url.clone()) .send() .await .with_context(|| format!("Failed to fetch url: {url}"))? .error_for_status() .with_context(|| format!("Failed to fetch url: {url}"))? .json::() .await .with_context(|| format!("Failed to fetch url: {url}"))?; trace!("Rebuilder search response: {search:#?}"); let mut attestations = attestation::Tree::default(); for record in search.records { let Some(build_id) = record.build_id else { continue; }; let Some(artifact_id) = record.artifact_id else { continue; }; let mut url = base_url.clone(); url.path_segments_mut() .map_err(|_| anyhow!("Failed to get path from url: {base_url}"))? .pop_if_empty() .push("api") .push("v1") .push("builds") .push(build_id.to_string().as_str()) .push("artifacts") .push(artifact_id.to_string().as_str()) .push("attestation"); debug!("Downloading attestation from rebuilder: {url}"); let response = self .get(url.clone()) .send() .await .with_context(|| format!("Failed to fetch url: {url}"))? .error_for_status() .with_context(|| format!("Failed to fetch url: {url}"))? .bytes() .await .with_context(|| format!("Failed to fetch url: {url}"))?; let attestation = Attestation::parse(&response) .with_context(|| format!("Failed to parse attestation from rebuilder: {url}"))?; attestations.insert(url.to_string(), attestation); } Ok(attestations) } } #[derive(Debug, Deserialize)] struct Search { records: Vec, } #[derive(Debug, Deserialize)] struct SearchRecord { build_id: Option, artifact_id: Option, } #[derive(Debug, Deserialize)] struct PublicKeys { current: Vec, } repro-threshold-0.1.2/src/inspect/deb.rs000064400000000000000000000105771046102023000163130ustar 00000000000000use crate::errors::*; use futures::StreamExt; use tokio::io::{AsyncBufRead, AsyncRead, AsyncReadExt, BufReader}; #[derive(Debug, PartialEq)] pub struct Deb { pub name: String, pub version: String, pub architecture: String, } enum Compression { Xz, } enum Decompressor { Xz(async_compression::tokio::bufread::XzDecoder), } impl Decompressor { fn new(reader: R, compression: Compression) -> Self { match compression { Compression::Xz => Self::Xz(async_compression::tokio::bufread::XzDecoder::new(reader)), } } } impl AsyncRead for Decompressor { fn poll_read( mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, buf: &mut tokio::io::ReadBuf<'_>, ) -> std::task::Poll> { match &mut *self { Decompressor::Xz(decoder) => std::pin::Pin::new(decoder).poll_read(cx, buf), } } } async fn extract_control_from_deb(reader: R) -> Result { let mut archive = tokio_ar::Archive::new(reader); while let Some(entry) = archive.next_entry().await { let entry = entry?; let Ok(name) = str::from_utf8(entry.header().identifier()) else { continue; }; // Determine compression let compression = match name.strip_prefix("control.tar.") { Some("xz") => Compression::Xz, Some(extension) => bail!("Found control.tar with unsupported extension: {extension}"), None => continue, }; // Setup decompression reader let reader = BufReader::new(entry); let decompressor = Decompressor::new(reader, compression); // Extract control file from control.tar.* return find_control_file(decompressor).await; } bail!("No control.tar found in .deb") } async fn find_control_file(reader: R) -> Result { let mut tar = tokio_tar::Archive::new(reader); let mut entries = tar .entries() .context("Failed to read entries from control.tar")?; while let Some(entry) = entries.next().await { let mut entry = entry.context("Failed to read entry from control.tar")?; let path = entry.path()?; trace!("Found entry in .deb: {path:?}"); if &*path != "./control" { continue; } let mut content = String::new(); entry .read_to_string(&mut content) .await .context("Failed to read control file from control.tar")?; return Ok(content); } bail!("No control file found in control.tar") } pub async fn inspect(reader: R) -> Result { let content = extract_control_from_deb(reader).await?; trace!("Control file content: {content:?}"); // now process the buffered data let deb822 = deb822_fast::Deb822::from_reader(content.as_bytes()) .map_err(|err| anyhow!("Failed to parse deb822: {err:#}"))?; let mut paragraphs = deb822.iter(); let paragraph = paragraphs .next() .ok_or_else(|| anyhow!("No paragraphs found in control file"))?; if paragraphs.next().is_some() { bail!("More than one paragraph found in control file"); } let name = paragraph .get("Package") .ok_or_else(|| anyhow!("No 'Package' field in paragraph"))?; let version = paragraph .get("Version") .ok_or_else(|| anyhow!("No 'Version' field in paragraph"))?; let architecture = paragraph .get("Architecture") .ok_or_else(|| anyhow!("No 'Architecture' field in paragraph"))?; let data = Deb { name: name.to_string(), version: version.to_string(), architecture: architecture.to_string(), }; debug!("Parsed .deb data: {data:?}"); Ok(data) } #[cfg(test)] mod tests { use super::*; use tokio::fs::File; #[tokio::test] async fn test_inspect_deb() { let file = File::open("test_data/librust-as-slice-dev_0.2.1-1+b2_amd64.deb") .await .unwrap(); let deb = inspect(file).await.unwrap(); assert_eq!( deb, Deb { name: "librust-as-slice-dev".to_string(), version: "0.2.1-1+b2".to_string(), architecture: "amd64".to_string(), } ); } } repro-threshold-0.1.2/src/inspect/mod.rs000064400000000000000000000000151046102023000163220ustar 00000000000000pub mod deb; repro-threshold-0.1.2/src/main.rs000064400000000000000000000030151046102023000150250ustar 00000000000000mod app; mod args; mod attestation; mod config; mod errors; mod event; mod http; mod inspect; mod plumbing; mod rebuilder; mod signing; mod transport; mod ui; mod withhold; use crate::app::App; use crate::args::{Args, SubCommand}; use crate::config::Config; use crate::errors::*; use clap::Parser; use env_logger::Env; use std::env; fn is_apt_transport_multicall() -> bool { let Some(bin) = env::args_os().next() else { return false; }; let Ok(bin) = bin.into_string() else { return false; }; let Some(bin) = bin.rsplit('/').next() else { return false; }; bin.starts_with("reproduced+") } #[tokio::main] async fn main() -> Result<()> { let args = Args::parse(); let log_level = match args.verbose { 0 => "repro_threshold=info", 1 => "info,repro_threshold=debug", 2 => "debug", 3 => "debug,repro_threshold=trace", _ => "trace", }; env_logger::init_from_env(Env::default().default_filter_or(log_level)); match args.subcommand { None if is_apt_transport_multicall() => transport::run(args::Transport::Apt).await, None => { let config = Config::load_writable().await?; let terminal = ratatui::init(); let result = App::new(config).run(terminal).await; ratatui::restore(); result } Some(SubCommand::Transport(transport)) => transport::run(transport).await, Some(SubCommand::Plumbing(plumbing)) => plumbing::run(plumbing).await, } } repro-threshold-0.1.2/src/plumbing.rs000064400000000000000000000145741046102023000157320ustar 00000000000000use crate::args::Plumbing; use crate::attestation; use crate::config::Config; use crate::errors::*; use crate::http; use crate::inspect; use crate::rebuilder; use crate::signing; use tokio::fs::File; use tokio::io::AsyncSeekExt; pub async fn run(plumbing: Plumbing) -> Result<()> { match plumbing { Plumbing::FetchRebuilderdCommunity => { let http = http::client(); for rebuilder in rebuilder::fetch_rebuilderd_community(&http).await? { let json = serde_json::to_string_pretty(&rebuilder)?; println!("{}", json); } } Plumbing::AddRebuilder { url, name } => { let mut config = Config::load_writable().await?; if let Some(rebuilder) = config.trusted_rebuilders.iter_mut().find(|r| r.url == url) { // we track selected rebuilders as copy in case they get deleted from e.g. the rebuilderd-community list // make sure the copy is also updated accordingly rebuilder.reconfigure(name.clone()); } if let Some(rebuilder) = config.custom_rebuilders.iter_mut().find(|r| r.url == url) { rebuilder.reconfigure(name); } else { let name = if let Some(name) = name { name.clone() } else { url.domain() .with_context(|| format!("Failed to detect domain from url: {url:?}"))? .to_string() }; let rebuilder = rebuilder::Rebuilder { name, url: url.clone(), distributions: vec![], country: None, contact: None, signing_keyring: String::new(), }; config.custom_rebuilders.push(rebuilder); } config.save().await?; } Plumbing::RemoveRebuilder { url } => { let mut config = Config::load_writable().await?; config.trusted_rebuilders.retain(|r| r.url != url); config.custom_rebuilders.retain(|r| r.url != url); config.save().await?; } Plumbing::ListRebuilders { all } => { let config = Config::load().await?; for rebuilder in config.resolve_rebuilder_view() { let status = if rebuilder.active { "[x]" } else if all { "[ ]" } else { continue; }; println!( "{} {:?} - {:?}", status, rebuilder.item.name, rebuilder.item.url ); } } Plumbing::AddBlindlyTrust { pkg } => { let mut config = Config::load_writable().await?; config.rules.blindly_trust.insert(pkg); config.save().await?; } Plumbing::RemoveBlindlyTrust { pkg } => { let mut config = Config::load_writable().await?; config.rules.blindly_trust.remove(&pkg); config.save().await?; } Plumbing::ListBlindlyTrust => { let config = Config::load().await?; for pkg in &config.rules.blindly_trust { println!("{pkg}"); } } Plumbing::Verify { signing_keys, attestations, rebuilders, threshold, file, } => { let path = &file; let mut file = File::open(path) .await .with_context(|| format!("Failed to open file {path:?}"))?; // Extract .deb metadata (if needed) let inspect = if !rebuilders.is_empty() { debug!("Inspecting package metadata: {path:?}"); // TODO: this is currently .deb only let inspect = inspect::deb::inspect(&mut file) .await .with_context(|| format!("Failed to inspect metadata: {path:?}"))?; file.rewind() .await .with_context(|| format!("Failed to rewind file after inspection: {path:?}"))?; Some(inspect) } else { None }; // Load all files from the local filesystem and await rebuilder responses let (sha256, mut attestations, remote_attestations, signing_keys) = tokio::try_join!( async { attestation::sha256_file(file) .await .with_context(|| format!("Failed to calculate hash for file: {path:?}")) }, async { Ok(attestation::load_all_attestations(&attestations).await) }, async { if let Some(inspect) = inspect { let http = http::client(); let attestations = attestation::fetch_remote(&http, rebuilders, inspect).await; Ok(attestations) } else { Ok(Default::default()) } }, async { signing::load_all_signing_keys(&signing_keys).await }, )?; // Merge local and remote attestations attestations.merge(remote_attestations); // Process all attestations for verification let confirms = attestations.verify(&sha256, &signing_keys); if confirms.len() >= threshold { info!( "Successfully verified attestations with {}/{} required signatures", confirms.len(), threshold ); } else { bail!( "Failed to verify attestations: only {}/{} required signatures", confirms.len(), threshold ); } } Plumbing::InspectDeb { file } => { let path = &file; let file = File::open(path) .await .with_context(|| format!("Failed to open file {path:?}"))?; let data = inspect::deb::inspect(file).await?; println!("data={data:#?}"); } Plumbing::Completions(completions) => { completions.generate(); } } Ok(()) } repro-threshold-0.1.2/src/rebuilder.rs000064400000000000000000000105761046102023000160700ustar 00000000000000use crate::errors::*; use crate::http; use crate::signing; use anyhow::Context; use in_toto::crypto::PublicKey; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use url::Url; const COMMUNITY_URL: &str = "https://raw.githubusercontent.com/kpcyrd/rebuilderd-community/refs/heads/main/README.md"; #[derive(Debug, Clone)] pub struct Selectable { pub active: bool, pub item: T, } impl From> for Selectable { fn from(selectable: Selectable<&T>) -> Self { Selectable { active: selectable.active, item: selectable.item.clone(), } } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Rebuilder { pub name: String, pub url: Url, pub distributions: Vec, pub country: Option, pub contact: Option, #[serde(default, skip_serializing_if = "String::is_empty")] pub signing_keyring: String, } impl Rebuilder { pub fn reconfigure(&mut self, name: Option) { if let Some(name) = name { self.name = name; } } pub async fn refresh_signing_keyring(&mut self, http: &http::Client) -> Result<()> { let keyring = http.fetch_signing_keyring(&self.url).await?; self.signing_keyring = keyring; Ok(()) } pub fn signing_key(&self) -> Result { let keyring_bytes = self.signing_keyring.as_bytes(); let mut keys = signing::pem_to_pubkeys(keyring_bytes)?; // Currently only the first key is considered keys.next() .context("No public keys found in signing keyring")? } } pub async fn fetch_rebuilderd_community(http: &http::Client) -> Result> { let response = http .get(COMMUNITY_URL) .send() .await? .error_for_status()? .text() .await?; parse(response.as_str()) } fn parse(text: &str) -> Result> { let mut start = None; let mut end = None; for (idx, line) in text.lines().enumerate() { if line.starts_with("```") { if start.is_none() { start = Some(idx + 1); } else if end.is_none() { end = Some(idx); break; } } } let start_line = start.context("Failed to find start of TOML data")?; let end_line = end.context("Failed to find end of TOML data")?; // Extract the lines between start and end let toml_content: Vec<&str> = text .lines() .skip(start_line) .take(end_line - start_line) .collect(); let toml_str = toml_content.join("\n"); let mut list = toml::from_str::>>(&toml_str)?; let list = list.remove("rebuilder").unwrap_or_default(); Ok(list) } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse() { let data = r#"# Rebuilderd Community Rebuilders this is `some text` ```toml [[rebuilder]] name = "Rebuilder One" url = "https://one.example.com" distributions = ["archlinux"] country = "DEU" contact = "Hello!" [[rebuilder]] name = "Rebuilder Two" url = "https://two.example.com" distributions = ["archlinux", "debian"] ``` "#; let rebuilders = parse(data).unwrap(); assert_eq!( rebuilders, &[ Rebuilder { name: "Rebuilder One".to_string(), url: "https://one.example.com".parse().unwrap(), distributions: vec!["archlinux".to_string()], country: Some("DEU".to_string()), contact: Some("Hello!".to_string()), signing_keyring: String::new(), }, Rebuilder { name: "Rebuilder Two".to_string(), url: "https://two.example.com".parse().unwrap(), distributions: vec!["archlinux".to_string(), "debian".to_string()], country: None, contact: None, signing_keyring: String::new(), }, ] ); } #[test] fn test_parse_empty() { let data = "```\n```"; let list = parse(data).unwrap(); assert_eq!(list, &[]); } #[test] fn test_parse_fully_empty() { let data = ""; let list = parse(data); assert!(list.is_err()); } } repro-threshold-0.1.2/src/signing.rs000064400000000000000000000200161046102023000155370ustar 00000000000000use crate::config::Config; use crate::errors::*; use in_toto::crypto::{KeyId, PublicKey, SignatureScheme}; use std::collections::{BTreeMap, BTreeSet}; use std::path::Path; use tokio::fs; use url::Host; const PEM_PUBLIC_KEY: &str = "PUBLIC KEY"; // Ensure each domain only gets one vote, until we don't have per-architecture rebuilders anymore pub struct DomainTree<'a> { map: BTreeMap, PublicKey)>, } impl<'a> DomainTree<'a> { pub fn from_config(config: &'a Config) -> Self { let mut map = BTreeMap::new(); for rebuilder in &config.trusted_rebuilders { let Ok(signing_key) = rebuilder.signing_key() else { continue; }; let key_id = signing_key.key_id().to_owned(); let Some(host) = rebuilder.url.host() else { continue; }; map.insert(key_id, (host, signing_key)); } DomainTree { map } } pub fn signing_keys(&self) -> impl Iterator { self.map.values().map(|(_, key)| key) } pub fn group_by_domain(&self, confirms: BTreeSet) -> BTreeSet { let mut voted = BTreeSet::new(); let mut new = BTreeSet::new(); for key_id in confirms { let Some((host, _)) = self.map.get(&key_id) else { continue; }; if voted.insert(host) { new.insert(key_id); } } new } } pub fn pem_to_pubkeys(buf: &[u8]) -> Result>> { let pems = pem::parse_many(buf).context("Failed to parse pem file")?; let iter = pems .into_iter() .filter(|pem| pem.tag() == PEM_PUBLIC_KEY) .map(|pem| { PublicKey::from_spki(pem.contents(), SignatureScheme::Ed25519) .context("Failed to parse signing key") }); Ok(iter) } pub async fn load_all_signing_keys, P: AsRef>( paths: I, ) -> Result> { let mut list = Vec::new(); for path in paths { let path = path.as_ref(); let signing_key = fs::read(&path) .await .with_context(|| format!("Failed to read signing keys: {path:?}"))?; let signing_keys = pem_to_pubkeys(&signing_key) .with_context(|| format!("Failed to parse signing keys: {path:?}"))?; list.extend(signing_keys.flatten()); } Ok(list) } #[cfg(test)] mod tests { use super::*; use crate::{ attestation::{self, Attestation}, rebuilder::Rebuilder, }; use std::str::FromStr; #[test] fn test_parse_signing_key() { let pem_data = include_bytes!("../test_data/reproducible-archlinux.pub"); let keys = pem_to_pubkeys(pem_data) .unwrap() .map(|key| key.map(|k| k.key_id().to_owned())) .collect::>>() .unwrap(); assert_eq!( keys, &[ "1ae6d32cb5bb8a98312106de28e50af7e09a9b294d51df459537908ac1288b8f" .parse() .unwrap() ] ); } #[test] fn test_domain_tree_grouping() { let mut attestations = attestation::Tree::default(); for attestation in [ r#"{"signatures":[{"keyid":"931cf71e1a72729f5d41957671508ffba5effe950aa7e7e2af4e99ec9dcde2ba","sig":"e34402178513bc9eb4053748f1dae437ec8368caee4d5f47a759159f60562b51c9112e693a9020f705178a891fd3119330601eea7119592bc23060007f9b1804"}],"signed":{"_type":"link","byproducts":{},"command":[],"environment":null,"materials":{},"name":"","products":{"file.bin":{"sha256":"59a6f8a560dc8a7f99f470570bcc100f50e415922fbf71a27af34c5630cf233a"}}}}"#, r#"{"signatures":[{"keyid":"1752ad72d6f07622d66da9676f5084385ab4e7a8af08bbe137d88dba5d0848f2","sig":"0ccf097506cd0dd06ad419fb417b35c526ec905f5af1418cb6e8abbf64d033ee3c1ea8bcded746d9a762dee0811770c1d67285a20717e93de19bff23c7f62604"}],"signed":{"_type":"link","byproducts":{},"command":[],"environment":null,"materials":{},"name":"","products":{"file.bin":{"sha256":"59a6f8a560dc8a7f99f470570bcc100f50e415922fbf71a27af34c5630cf233a"}}}}"#, r#"{"signatures":[{"keyid":"c2b6844adec1b4debbdeb606a42b8ed93444344326afad4af20f53bc1068e6e9","sig":"52ed7f2018bf2242ac09561b31eac87a844b93429b9050a76c72989e58ad3948ebde0629c24828c0970d33a8cada70eefb5606e2d5bb28149ad4a7e378c9e608"}],"signed":{"_type":"link","byproducts":{},"command":[],"environment":null,"materials":{},"name":"","products":{"file.bin":{"sha256":"59a6f8a560dc8a7f99f470570bcc100f50e415922fbf71a27af34c5630cf233a"}}}}"#, r#"{"signatures":[{"keyid":"c2b6844adec1b4debbdeb606a42b8ed93444344326afad4af20f53bc1068e6e9","sig":"52ed7f2018bf2242ac09561b31eac87a844b93429b9050a76c72989e58ad3948ebde0629c24828c0970d33a8cada70eefb5606e2d5bb28149ad4a7e378c9e608"}],"signed":{"_type":"link","byproducts":{},"command":[],"environment":null,"materials":{},"name":"","products":{"file.bin":{"sha256":"59a6f8a560dc8a7f99f470570bcc100f50e415922fbf71a27af34c5630cf233a"}}}}"#, ] { let attestation = Attestation::parse(attestation.as_bytes()).unwrap(); attestations.insert("".to_string(), attestation); } let config = Config { trusted_rebuilders: vec![ Rebuilder { name: "A".to_string(), url: "https://rebuilder.example.com".parse().unwrap(), distributions: Default::default(), country: None, contact: None, signing_keyring: "-----BEGIN PUBLIC KEY-----\r\nMCwwBwYDK2VwBQADIQAO2E6IRl1NbzFuNQ8tDeii85GknnvibBj+AmQDSiYVkg==\r\n-----END PUBLIC KEY-----\r\n".to_string(), }, Rebuilder { name: "B".to_string(), url: "https://rebuilder.example.com".parse().unwrap(), distributions: Default::default(), country: None, contact: None, signing_keyring: "-----BEGIN PUBLIC KEY-----\r\nMCwwBwYDK2VwBQADIQC+uldtf6F9pI5IYY3p0IzzQSnh/uRZS8c1NmxW3/zP/g==\r\n-----END PUBLIC KEY-----\r\n".to_string(), }, Rebuilder { name: "C".to_string(), url: "https://another-rebuilder.example.org".parse().unwrap(), distributions: Default::default(), country: None, contact: None, signing_keyring: "-----BEGIN PUBLIC KEY-----\r\nMCwwBwYDK2VwBQADIQCjiKUEanhTIjz+VDQ22bEWiMVSgDvsqwSAr1zqAuUKlw==\r\n-----END PUBLIC KEY-----\r\n".to_string(), }, ], ..Default::default() }; let trusted = DomainTree::from_config(&config); let confirms = attestations.verify( &[ 0x59, 0xa6, 0xf8, 0xa5, 0x60, 0xdc, 0x8a, 0x7f, 0x99, 0xf4, 0x70, 0x57, 0x0b, 0xcc, 0x10, 0x0f, 0x50, 0xe4, 0x15, 0x92, 0x2f, 0xbf, 0x71, 0xa2, 0x7a, 0xf3, 0x4c, 0x56, 0x30, 0xcf, 0x23, 0x3a, ], trusted.signing_keys(), ); assert_eq!( confirms, BTreeSet::from_iter([ KeyId::from_str("1752ad72d6f07622d66da9676f5084385ab4e7a8af08bbe137d88dba5d0848f2") .unwrap(), KeyId::from_str("931cf71e1a72729f5d41957671508ffba5effe950aa7e7e2af4e99ec9dcde2ba") .unwrap(), KeyId::from_str("c2b6844adec1b4debbdeb606a42b8ed93444344326afad4af20f53bc1068e6e9") .unwrap(), ]) ); let filtered = trusted.group_by_domain(confirms); assert_eq!( filtered, BTreeSet::from_iter([ KeyId::from_str("1752ad72d6f07622d66da9676f5084385ab4e7a8af08bbe137d88dba5d0848f2") .unwrap(), KeyId::from_str("c2b6844adec1b4debbdeb606a42b8ed93444344326afad4af20f53bc1068e6e9") .unwrap(), ]) ); } } repro-threshold-0.1.2/src/transport/alpm.rs000064400000000000000000000001701046102023000170650ustar 00000000000000use crate::config::Config; use crate::errors::*; pub async fn run(_config: Config) -> Result<()> { todo!("alpm") } repro-threshold-0.1.2/src/transport/apt.rs000064400000000000000000000143121046102023000167230ustar 00000000000000use crate::attestation; use crate::config::Config; use crate::errors::*; use crate::http; use crate::inspect; use crate::signing::DomainTree; use crate::withhold; use std::collections::BTreeMap; use tokio::fs::File; use tokio::io::{self, AsyncBufRead, AsyncBufReadExt, BufReader}; use url::Url; #[derive(Debug, Default)] struct Request { status: String, headers: BTreeMap, } impl Request { async fn read(mut reader: R) -> Result> { let mut buf = String::new(); let mut req = Request::default(); loop { let n = reader.read_line(&mut buf).await?; // read command if n == 0 { return Ok(None); } let line = buf.trim_end(); trace!("Read line: {line:?}"); if req.status.is_empty() { req.status = line.to_string(); } else if line.is_empty() { return Ok(Some(req)); } else if let Some((key, value)) = line.split_once(": ") { req.headers.insert(key.to_string(), value.to_string()); } buf.clear(); } } fn needs_verification(&self) -> bool { match self.headers.get("Target-Type").map(String::as_str) { Some("deb") | None => true, Some("index") => false, // We don't recognize this type, but it doesn't seem to be a .deb so should be fine Some(_other) => false, } } } /// For safety reasons, make sure we absolutely do not have newlines in the messages fn truncate_newline(s: &str) -> &str { s.split_once('\n').map(|(line, _)| line).unwrap_or(s) } fn uri_failure(uri: Option<&str>, message: &str) { println!("400 URI Failure"); println!("Message: {}", truncate_newline(message)); if let Some(uri) = uri { println!("URI: {}", truncate_newline(uri)); } println!(); } fn send_status(uri: &str, message: &str) { println!("102 Status"); println!("Message: {}", truncate_newline(message)); println!("URI: {}", truncate_newline(uri)); println!(); } async fn acquire(http: &http::Client, config: &Config, req: &Request) -> Result<()> { let uri = req.headers.get("URI").context("Missing `URI` header")?; let filename = req .headers .get("Filename") .context("Missing `Filename` header")?; let url = uri.strip_prefix("reproduced+").unwrap_or(uri); let url = url.parse::().context("Invalid URI")?; let domain = url.domain().context("URI missing domain")?; // Open file for writing let file = File::options() .create(true) .read(true) .write(true) .truncate(true) .open(filename) .await .with_context(|| format!("Failed to open file: {}", filename))?; let mut file = withhold::Writer::new(file); // Start sending request send_status(uri, &format!("Connecting to {}", domain)); let mut response = http.get(url).send().await?.error_for_status()?; let last_modified = response .headers() .get("Last-Modified") .and_then(|v| v.to_str().ok()) .map(String::from); println!("200 URI Start"); if let Some(last_modified) = &last_modified { println!("Last-Modified: {}", truncate_newline(last_modified)); } println!("URI: {}", truncate_newline(uri)); println!(); while let Some(chunk) = response.chunk().await.transpose() { file.write_all(chunk?).await?; } let sha256 = file.sha256(); // Verify reproducible builds attestations if req.needs_verification() { send_status(uri, "Verifying download"); let mut reader = file.into_reader().await?; // Parse deb metadata let inspect = inspect::deb::inspect(&mut reader) .await .context("Failed to parse .deb metadata")?; file = reader.into_writer().await?; if !config.rules.blindly_trust.contains(&inspect.name) { // Fetch attestations let rebuilders = config.trusted_rebuilders.iter().map(|r| r.url.clone()); let attestations = attestation::fetch_remote(http, rebuilders, inspect).await; // Ensure each domain only gets one vote, until we don't have per-architecture rebuilders anymore let trusted = DomainTree::from_config(config); let confirms = attestations.verify(&sha256, trusted.signing_keys()); let confirms = trusted.group_by_domain(confirms); if confirms.len() < config.rules.required_threshold { bail!( "Not enough reproducible builds attestations: only {}/{} required signatures", confirms.len(), config.rules.required_threshold ); } } } // If successfully verified, write final chunk file.finalize().await?; println!("201 URI Done"); println!("SHA256-Hash: {}", data_encoding::HEXLOWER.encode(&sha256)); if let Some(last_modified) = &last_modified { println!("Last-Modified: {}", truncate_newline(last_modified)); } println!("Size: {}", file.size()); println!("Filename: {}", truncate_newline(filename)); println!("URI: {}", truncate_newline(uri)); println!(); Ok(()) } pub async fn run(config: Config) -> Result<()> { println!("100 Capabilities"); println!("Send-URI-Encoded: true"); // println!("Send-Config: true"); // println!("Pipeline: true"); println!("Version: 1.2"); println!(); let http = http::client(); let mut stdin = BufReader::new(io::stdin()); while let Some(req) = Request::read(&mut stdin).await? { if req.status.starts_with("600 ") { debug!("Received acquire request: {req:?}"); // 600 URI Acquire if let Err(err) = acquire(&http, &config, &req).await { uri_failure( req.headers.get("URI").map(|s| s.as_str()), &format!("{err:#}"), ); } } else if req.status.starts_with("601 ") { // 601 Configuration } else { uri_failure(None, &format!("Unsupported command: {}", req.status)); } } Ok(()) } repro-threshold-0.1.2/src/transport/mod.rs000064400000000000000000000005251046102023000167170ustar 00000000000000pub mod alpm; pub mod apt; use crate::args::Transport; use crate::config::Config; use crate::errors::*; pub async fn run(transport: Transport) -> Result<()> { let config = Config::load().await?; match transport { Transport::Alpm { .. } => alpm::run(config).await, Transport::Apt => apt::run(config).await, } } repro-threshold-0.1.2/src/ui/blindly.rs000064400000000000000000000030411046102023000161520ustar 00000000000000use crate::app::App; use crate::ui::{self, SELECTED_STYLE}; use ratatui::{ prelude::*, widgets::{HighlightSpacing, List, ListItem, Scrollbar, ScrollbarOrientation, ScrollbarState}, }; use std::iter; impl App { pub fn render_blindly_trust(&mut self, area: Rect, buf: &mut Buffer) { let block = ui::container(); let items = iter::once(ListItem::from(Span::styled( "Use `repro-threshold plumbing [add-blindly-trust|remove-blindly-trust] ` to update", Style::new().italic() ))) .chain( self.config .rules .blindly_trust .iter() .map(|s| ListItem::from(format!("Always blindly trust: {s}"))), ) .collect::>(); let list = List::new(items) .block(block) .highlight_style(SELECTED_STYLE) .highlight_symbol("> ") .highlight_spacing(HighlightSpacing::Always); StatefulWidget::render(&list, area, buf, self.scroll()); Scrollbar::new(ScrollbarOrientation::VerticalRight) .begin_symbol(None) .end_symbol(None) .track_symbol(None) .render( area.inner(Margin { horizontal: 0, vertical: 1, }), buf, &mut ScrollbarState::new(list.len()) .position(self.scroll().selected().unwrap_or_default()), ); } } repro-threshold-0.1.2/src/ui/home.rs000064400000000000000000000032531046102023000154520ustar 00000000000000use crate::app::App; use crate::ui::{self, COLOR_NEGATIVE, COLOR_POSITIVE, COLOR_WARNING, SELECTED_STYLE}; use ratatui::{ prelude::*, widgets::{HighlightSpacing, List, ListItem}, }; impl App { pub fn render_home(&mut self, area: Rect, buf: &mut Buffer) { let block = ui::container(); let required_threshold = self.config.rules.required_threshold; let trusted_rebuilders = self.config.trusted_rebuilders.len(); let items = vec![ ListItem::new(Line::from_iter([ Span::raw("Required reproduction threshold: "), Span::styled( required_threshold.to_string(), match required_threshold { 0 => COLOR_NEGATIVE, 1 => COLOR_WARNING, num if num <= trusted_rebuilders => COLOR_POSITIVE, _ => COLOR_NEGATIVE, }, ), Span::raw("/"), Span::raw(format!("{trusted_rebuilders}")), ])), ListItem::new(format!( "Configure trusted rebuilders ({trusted_rebuilders} selected)" )), ListItem::new(format!( "Add/remove packages from 'blindly-trust' set ({} entries)", self.config.rules.blindly_trust.len() )), ListItem::new("Quit"), ]; let list = List::new(items) .block(block) .highlight_style(SELECTED_STYLE) .highlight_symbol("> ") .highlight_spacing(HighlightSpacing::Always); StatefulWidget::render(&list, area, buf, self.scroll()); } } repro-threshold-0.1.2/src/ui/mod.rs000064400000000000000000000035141046102023000153010ustar 00000000000000mod blindly; mod home; mod rebuilders; use crate::app::App; use ratatui::{ layout::Flex, prelude::*, widgets::{Block, BorderType, Clear}, }; const SELECTED_STYLE: Style = Style::new().bg(Color::Reset).add_modifier(Modifier::BOLD); const COLOR_POSITIVE: Color = Color::Green; const COLOR_WARNING: Color = Color::Yellow; const COLOR_NEGATIVE: Color = Color::Red; const TITLE: &str = concat!( "repro-threshold ", env!("CARGO_PKG_VERSION"), " (experimental)" ); const TITLE_STYLE: Style = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD); fn container() -> Block<'static> { Block::bordered() .title(TITLE) .title_alignment(Alignment::Center) .title_style(TITLE_STYLE) .border_type(BorderType::Rounded) } impl Widget for &mut App { fn render(self, area: Rect, buf: &mut Buffer) { match self.view { Some(crate::app::View::Home) => self.render_home(area, buf), Some(crate::app::View::Rebuilders { .. }) => self.render_rebuilders(area, buf), Some(crate::app::View::BlindlyTrust { .. }) => self.render_blindly_trust(area, buf), None => {} } if self.confirm { let popup = Block::bordered().title("Are you sure?"); let popup_area = centered_area(area, 60, 40); // clears out any background in the area before rendering the popup Clear.render(popup_area, buf); popup.render(popup_area, buf); } } } fn centered_area(area: Rect, percent_x: u16, percent_y: u16) -> Rect { let vertical = Layout::vertical([Constraint::Percentage(percent_y)]).flex(Flex::Center); let horizontal = Layout::horizontal([Constraint::Percentage(percent_x)]).flex(Flex::Center); let [area] = area.layout(&vertical); let [area] = area.layout(&horizontal); area } repro-threshold-0.1.2/src/ui/rebuilders.rs000064400000000000000000000051021046102023000166550ustar 00000000000000use crate::app::App; use crate::rebuilder::{Rebuilder, Selectable}; use crate::ui::{self, COLOR_POSITIVE, SELECTED_STYLE}; use ratatui::{ prelude::*, widgets::{HighlightSpacing, List, ListItem, Scrollbar, ScrollbarOrientation, ScrollbarState}, }; impl App { pub fn render_rebuilders(&mut self, area: Rect, buf: &mut Buffer) { let block = ui::container(); let items = if self.rebuilders.is_empty() { vec![ListItem::new(Span::styled( "No rebuilders configured, press ctrl-R to load community set, or run `repro-threshold plumbing add-rebuilder ` to add one", Style::new().italic(), ))] } else { self.rebuilders .iter() .map(ListItem::from) .collect::>() }; let list = List::new(items) .block(block) .highlight_style(SELECTED_STYLE) .highlight_symbol("> ") .highlight_spacing(HighlightSpacing::Always); StatefulWidget::render(&list, area, buf, self.scroll()); Scrollbar::new(ScrollbarOrientation::VerticalRight) .begin_symbol(None) .end_symbol(None) .track_symbol(None) .render( area.inner(Margin { horizontal: 0, vertical: 1, }), buf, &mut ScrollbarState::new(list.len()) .position(self.scroll().selected().unwrap_or_default()), ); } } impl From<&Selectable> for ListItem<'_> { fn from(value: &Selectable) -> Self { let mut line = Line::from_iter([ if value.active { Span::styled("✓", COLOR_POSITIVE) } else { Span::raw("☐") }, Span::raw(format!( " {} - {}", value.item.name.escape_default(), value.item.url )), ]); if !value.item.distributions.is_empty() { line.push_span(Span::raw(" [")); for (i, dist) in value.item.distributions.iter().enumerate() { if i > 0 { line.push_span(Span::raw(", ")); } line.push_span(Span::raw(dist.escape_default().to_string())); } line.push_span(Span::raw("]")); } if let Ok(key) = value.item.signing_key() { line.push_span(Span::raw(format!(" - {:?}", key.key_id()))); } ListItem::new(line) } } repro-threshold-0.1.2/src/withhold.rs000064400000000000000000000154061046102023000157320ustar 00000000000000use crate::errors::*; use bytes::Bytes; use sha2::{Digest, Sha256}; use std::{io::SeekFrom, pin::Pin, task::Poll}; use tokio::io::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWrite, AsyncWriteExt}; pub struct Writer { inner: W, withheld: Option, size: u64, sha256: Sha256, } impl Writer { pub fn new(inner: W) -> Self { Self { inner, withheld: None, size: 0, sha256: Sha256::new(), } } async fn apply(&mut self, chunk: &[u8]) -> Result<()> { self.inner.write_all(chunk).await?; self.size += chunk.len() as u64; self.sha256.update(chunk); Ok(()) } pub async fn write_all(&mut self, chunk: Bytes) -> Result<()> { if let Some(chunk) = self.withheld.replace(chunk) { self.apply(&chunk).await?; } Ok(()) } pub fn size(&self) -> u64 { if let Some(chunk) = &self.withheld { self.size + chunk.len() as u64 } else { self.size } } pub fn sha256(&self) -> Vec { let mut sha256 = self.sha256.clone(); if let Some(chunk) = &self.withheld { sha256.update(chunk); } sha256.finalize().to_vec() } pub async fn finalize(&mut self) -> Result<()> { if let Some(chunk) = self.withheld.take() { self.apply(&chunk).await?; } self.inner.flush().await?; Ok(()) } } impl Writer { pub async fn into_reader(self) -> Result> { let mut file = self.inner; let writer = Writer { inner: (), withheld: self.withheld, size: self.size, sha256: self.sha256, }; let old_position = file .stream_position() .await .context("Failed to get position")?; file.rewind().await.context("Failed to rewind file")?; Ok(Reader { inner: file, cursor: 0, old_position, writer, }) } } pub struct Reader { inner: R, cursor: u64, old_position: u64, writer: Writer<()>, } impl Reader { fn peek_withheld(&self, limit: usize) -> Option<&[u8]> { let cursor = self.cursor.checked_sub(self.old_position)?; let withheld = self.writer.withheld.as_ref()?; let bytes = withheld.get(cursor as usize..)?; let bytes = bytes.get(..limit).unwrap_or(bytes); Some(bytes) } } impl AsyncRead for Reader { fn poll_read( mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, buf: &mut tokio::io::ReadBuf<'_>, ) -> Poll> { if self.cursor >= self.old_position { if let Some(slice) = self.peek_withheld(buf.remaining()) { // Has some withheld data (still) let num_bytes = slice.len() as u64; buf.put_slice(slice); self.cursor += num_bytes; } Poll::Ready(Ok(())) } else { let filled_before = buf.filled().len() as u64; match Pin::new(&mut self.inner).poll_read(cx, buf) { Poll::Ready(Ok(())) => { let filled_after = buf.filled().len() as u64; let bytes_read = filled_after - filled_before; self.cursor += bytes_read; Poll::Ready(Ok(())) } Poll::Ready(Err(e)) => Poll::Ready(Err(e)), Poll::Pending => Poll::Pending, } } } } impl Reader { pub async fn into_writer(self) -> Result> { let mut file = self.inner; file.seek(SeekFrom::Start(self.old_position)) .await .context("Failed to seek to old position")?; Ok(Writer { inner: file, withheld: self.writer.withheld, size: self.writer.size, sha256: self.writer.sha256, }) } } #[cfg(test)] mod tests { use super::*; use std::io::Cursor; use tokio::io::AsyncReadExt; #[tokio::test] async fn test_withhold_writer() -> Result<()> { let data = b"Hello, world!"; let mut buf = Vec::new(); let mut writer = Writer::new(&mut buf); writer.write_all(Bytes::from(&data[..5])).await?; writer.write_all(Bytes::from(&data[5..])).await?; assert_eq!(writer.size(), data.len() as u64); let sha256 = writer.sha256(); assert_eq!( data_encoding::HEXLOWER.encode(&sha256), "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3" ); writer.finalize().await?; assert_eq!(writer.size(), data.len() as u64); let sha256 = writer.sha256(); assert_eq!( data_encoding::HEXLOWER.encode(&sha256), "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3" ); assert_eq!(&buf[..], data); Ok(()) } #[tokio::test] async fn test_withhold_writer_reader() -> Result<()> { let data = b"Hello, world!"; let mut buf = Cursor::new(Vec::new()); let mut writer = Writer::new(&mut buf); writer.write_all(Bytes::from(&data[..5])).await?; writer.write_all(Bytes::from(&data[5..])).await?; let mut reader = writer.into_reader().await?; assert_eq!(reader.inner.get_ref(), b"Hello"); let mut text = String::new(); reader.read_to_string(&mut text).await?; assert_eq!(text, "Hello, world!"); assert_eq!(reader.inner.get_ref(), b"Hello"); let mut writer = reader.into_writer().await?; writer.finalize().await?; assert_eq!(buf.get_ref(), data); Ok(()) } #[test] fn test_peek_withheld() { let mut reader = Reader { inner: Cursor::new(Vec::new()), cursor: 0, old_position: 4, writer: Writer { inner: (), withheld: Some(Bytes::from("withheld data")), size: 0, sha256: Sha256::new(), }, }; // Try peek while still inside file data assert_eq!(reader.peek_withheld(5), None); // Update cursor to start with withheld data reader.cursor = 4; assert_eq!(reader.peek_withheld(50), Some(b"withheld data".as_ref())); // Try with smaller limit assert_eq!(reader.peek_withheld(3), Some(b"wit".as_ref())); // Increment cursor further reader.cursor = 10; assert_eq!(reader.peek_withheld(4), Some(b"ld d".as_ref())); } } repro-threshold-0.1.2/test_data/filesystem-2025.10.12-1-any.INVALID.in-toto.link000064400000000000000000000021431046102023000244060ustar 00000000000000{"signatures":[{"keyid":"7e99d2c42018d60df4e14bbbc513dd378eb5dedc6a1eeb73465f1d670a76c770","sig":"abc12345a1c0124d75086feab4b027aa66b1d3dd3c30b44f8366259bcbe60dc56a652d37b050b1dd54f3df4406b7a88e117c26633a9b7b010e9beec5cbf5630c"},{"keyid":"1ae6d32cb5bb8a98312106de28e50af7e09a9b294d51df459537908ac1288b8f","sig":"abc1234535bf46b04fcd1f7fd93807dccc7879a428e6951a9fc4c7d692914d33220c826bea6e326bd36dda8ed78d053d795541741afea8bd80e5ce14d31f4d06"}],"signed":{"_type":"link","name":"rebuild filesystem-2025.10.12-1-any.pkg.tar.zst","materials":{"filesystem-2025.10.12-1-any.pkg.tar.zst":{"sha512":"3bafca159d3ee55701331acac478de23e4d4bce8ca45c1dcc75a4b234fcbd36b3d72f30398ea5cd4fd089e35258a0699eae75a5b6d9b4f5ec62b87b8b997691e","sha256":"6b6c3fee7432204840d3b6afc9bc1a68c28f591a47fb220071715c40cca956df"}},"products":{"filesystem-2025.10.12-1-any.pkg.tar.zst":{"sha512":"3bafca159d3ee55701331acac478de23e4d4bce8ca45c1dcc75a4b234fcbd36b3d72f30398ea5cd4fd089e35258a0699eae75a5b6d9b4f5ec62b87b8b997691e","sha256":"6b6c3fee7432204840d3b6afc9bc1a68c28f591a47fb220071715c40cca956df"}},"environment":null,"byproducts":{},"command":[]}} repro-threshold-0.1.2/test_data/filesystem-2025.10.12-1-any.in-toto.link000064400000000000000000000021421046102023000233200ustar 00000000000000{"signatures":[{"keyid":"7e99d2c42018d60df4e14bbbc513dd378eb5dedc6a1eeb73465f1d670a76c770","sig":"455768f0a1c0124d75086feab4b027aa66b1d3dd3c30b44f8366259bcbe60dc56a652d37b050b1dd54f3df4406b7a88e117c26633a9b7b010e9beec5cbf5630c"},{"keyid":"1ae6d32cb5bb8a98312106de28e50af7e09a9b294d51df459537908ac1288b8f","sig":"5125ba1d35bf46b04fcd1f7fd93807dccc7879a428e6951a9fc4c7d692914d33220c826bea6e326bd36dda8ed78d053d795541741afea8bd80e5ce14d31f4d06"}],"signed":{"_type":"link","name":"rebuild filesystem-2025.10.12-1-any.pkg.tar.zst","materials":{"filesystem-2025.10.12-1-any.pkg.tar.zst":{"sha512":"3bafca159d3ee55701331acac478de23e4d4bce8ca45c1dcc75a4b234fcbd36b3d72f30398ea5cd4fd089e35258a0699eae75a5b6d9b4f5ec62b87b8b997691e","sha256":"6b6c3fee7432204840d3b6afc9bc1a68c28f591a47fb220071715c40cca956df"}},"products":{"filesystem-2025.10.12-1-any.pkg.tar.zst":{"sha512":"3bafca159d3ee55701331acac478de23e4d4bce8ca45c1dcc75a4b234fcbd36b3d72f30398ea5cd4fd089e35258a0699eae75a5b6d9b4f5ec62b87b8b997691e","sha256":"6b6c3fee7432204840d3b6afc9bc1a68c28f591a47fb220071715c40cca956df"}},"environment":null,"byproducts":{},"command":[]}}repro-threshold-0.1.2/test_data/filesystem-2025.10.12-1-any.pkg.tar.zst000064400000000000000000000361671046102023000231760ustar 00000000000000(/xD-V9$M lV"$ wjg K#<>MsXt./|Ϫ& f$oX Q (a VNx4H=59w-|912&95o:M򔆝[$}nATP26a(r-QG~d2Sgac ;{5rjvfBcdBTGZnOơY]}YBOis|{|obĕ Ʊ>o/]TO\L*nBc VIV>K}J mwPZH0_{CږGG$@bqKۮeюʨ3n.p7t8HG/66.fನ+?,/Ď&c'}T7Tra9s$IR aB bҒB!" bT4%ERETbw^7NI"^|>`FB{w/[Y®yQz&iX_2BMp ^x5bi⩷qk/au$#q͕<^,kpx$JlzE.y8ɟ1/hriPKWI~(9e9THbI؈l%&.*QB˱DUS#Voc%eSc_ECr v) VYm6](:[C9UlLRML+/p9Drrv [-y]p?PvXx,|>gp|F/Wsٗ@~cX.xWowdbf*ّN%$J(CEHw(-\ػeiً jpOƜ fbbH![}TԦD9EDٗk nM9E&98M6yκXBg4[)֔T4>춇Y 2E '㇢cѵj_y-\Ͽr\c@\Lo&ZT,6%ŽFWP+.&#)j\,x]%dl7mdOJxE`k[Ř"Phk.z\M]΍~Tt*-!1E'&23Cɵ&ɨ$ٹˮ;>[ɡڬU(xdhp^gB[1mȏ@7m݋bT 4⌘fys䂎n{lcGWmJeLjL 7ȩW.ζDzn|6aLT?!b_aB7 2+rkJՆtueI9d2{f;cFqeŷfz(I@2+ tKo={ZHf^%AC8o}_(<Q#^IRMsVVUU832MUq$-]K{tjq=D8RNvɧH7 <ў~lb-َFqTT!XEQ%zB)6 p|8B3(KIVF]O˔z3@,OZ8?gOMw x $tn7Eg,m?ɾۋ2Nj>LT2=׎zJu"y1hͳW^o͠rގk)Czpb۝Q5Q'ݠ=ΕI A@c:DުWKCm]oDx*ezm/鉝1$R*F x} vp-A$W`zR}%sBِXiD{Y_~o<|nYf ~%MgDAkv`&j h͆52 *wd]q⊋Mc8B( |R*B@ABR _ _8J$8B]@@]|oW`T^ۤ9hhA I]Khɀz/{ tz2ik-/ģex4]ŇiG]x&3 N] {qLL q۫W &~Hn:Mc0{7Ǎa|$:Q|̈pMӿ0>`8LcL(6E3qq6w=-NJo1oAE6ݤj9<*_ϿYXr#C'u}Ijjze~U;۩)Y2yJ{{ 𜽿>\iOWNjY8/鞖?;J9v/o+>uR}f֟C{C{=m- W RY4Qш T#e!7)B<HX,:Tm?96MU]Du B3E*tRAV2܃q$J1sJt&CZ+3 $%[ (=n)Oh%tF b ^s~co|"|n ;l ּHv:.֟Hgpy|cӛi?sC(ϟ |k6B_A;|mخ덌7wWq9?e,<).PKG1266215# Generatby # us s xtype=descBAL s://siz24551 ln0BSD bup/tabfsuphost.sield.sowipadroresseyellsubgidude7773043 2usr/b/552 51-rele70013223363# Cigu elocks.See (5)tailNOTE: Do n your (/)here, it mube set up1s)H?@;"! ABP I84aͶR94*N @&ͼ/K I[6%g2•j():X!Ogg r @U" \C.$.k> mjTG!ִx7Xշ ^%<$D#U _=<?ׅaW2,@-YkQ*ek ĉc8*{@߷==uqGފp+nn?$aϲZzc85L}}YECC,4-~Fs5լOeU(ZZC5SDIRPȐ My‘0!H@c"H$H$ќ<uP[) o5jd"@32w$-Q{g_ ׁapKïx~ X^rҡ;4ݘ`4jn N N{9srڀ~d%Z"i/Bʰ»BpXlQP=TjAt Wf:moP % ,Wb_2lgX Z ]HlfAai#ـEˌy˙b `xЄ N>r+ j_.NJn -]H|x> ȈhѾJaiwa0ހpܤ<2hgׁwo) s.7=XrMT1iȲ _DKǤD AQw>:f#ؕM4UE )h4Bz.VT%οdbl@Tz죌 y%y޷~cw{yn|HjXb !k8l0 /@V_P{룾YG+x7vknHH[P & BB$0E"}/uUUx[nm |ERXuI0eE2;~y5%$g2Nyİ>4lR4Q.d*T0Cu*xNU4a0ţ\:Gá( +q#pR (Ux]l"@ Fɢ0ȶi +᭹a pT37tȎX3T5~m*iw'?}PGt: 3ql:XDx:# kΤmŋkkE⠼0YЈwqXqWnn5vƲ,Hl% fQP: ,QÎC328xC<ɚ&$'JlJ0*6M:GɐPor&48m0 ˦5e6&:bfqܤ4+{8$`w ٌ$ WŚ9,UXwȿ0qZOd+Xhq_rĢ +ɢ_`$%dCo /V4:>ӴF_?朐7cO=WHk8TEv1aq#ER&:DQ3./ UiĕESӺ:ZpgLaxoZɤEeEQW2$Ik(QA%-~(D1!D  $P$){o-% l@[$n;&/.maADe<hX6 ZG ' pbrphw+߮UF5V.*>&nNѣ:"cՋ*ml\ĭDg3iz5L2C8ؙBS:%!%,,Zdw^GSGx 4-Jx^x,C926g(f6iT`\X0>,2oxY$la!;1ro60MD; 7kIR ڙ=71!0DcH›>@4o08B5pNZmNW_Jc6z"-`V 0ڀ{풁"^{JNx1u2C*y VH@첓QC~h,|>'gڅZ?'\J*P<)R9[\@_d C _ؠA^: &&8bl@-id[[ 4Q0c$K1"<Qoː hT/dch'2$ˣc%7ǀYu$]`5,ـy x\5t p`шlByP#~mvbO Tx?L Iώڧ|μϮG'!," kY@/lwOt暠7USpAO9pt\A&:c:QA DP(' |&q49з^d!GعE+#`-Y !} ^yMhbc`aXp abnZB%ŲQ|qK^F9Vwi PrQgZPrL$pڈGPYi-DP~5!N:Q cEK |1cҰƌ $ (E|^J3v@r&^[ (4n8 ɏ-z@#Rz&.3V!7 }E&QP$rpXF 1̫ 1vٙ d=t4?ـv]jI _OZzk_}V{Cz;u(Z̈ e m[d\@, t[7:On>03$FN4a5,s|=: YC0H֑56 X+:9Z/8-0"4Iۖkhy>-^!}G9H1>ESh!@pz)y>X,[ P! 1u`0{WjLa(A E##+O:TׁӼ܂u6UF`{W_KN8J%X>):kX6g}紳GN5/j>x.gwer2$Iv!9f UuBB!#D@cT'i=(k #A$azMp|6'ltp=>ޤ PQq2ŜޠZő I,aJPmiZ\ *ƻLfpM`) }?%`@euC[.]WQ>NR7/`UdUۀ)4>= [@@$ l2ۀtINׄˑ9 "= w@siz Hi@Zӌ-hCMKnd*7 w- g}Y_L0@򠨊&NiEhnnC{W2B SyFqy'JWj@Ti酅%@B Cs4N crFo*Vu uEʙU5&8dYCi㤫V?{6Gs^AZ2C Fq[J"F~(3u11y *)BbC9.;OVotgt\vkjO͞W>'KՃ, RKt^1ԡ\BhvW-!3 vҶlG~hB_:}P,X?7-L6 Kc{Hc<\prU)\XuvnDd ] @5mL 6" h?ʹK5ӿUL+5]byKtdA!UEmc+GU'A| -ޱTȗ7N&#E*٭5_)uvNqT?-ۻ& VX) !ӫ,_{ɲa[,ۍajĽДщtRNS  !"#$%&'()*+,-./0126:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~< I XuwK*<6V3mͮ 45ڊZ6kףUL-#[W,۬6SZ+-( Mf}EE=.-YK<^V +̳úʍ_XVT aUGEF.ͣV6AڗR3C˨6N|%5t+t'ZʶCg;QЙuOЕ먵qЕOp0j/ aNjIɣ?@'?@B(.<=hqR:0k{L-?53INvQ7uqZW._qQkEG)an-_(.wnqP܃B kP\A8b7pa)ve &^B%E9B[]ͭWHw']GaPf9O)(EaR]MqqwJqf/Oqld'(n8}AD&@#)n"NBK(n_84% u0 Z8WyS7ԃ2fC}I셔Pjmq2^!j6e8]ጀzR!zCM(e#9̌"6d#4r2 nl`m.3-'YsmA5(i-GkX%@G!Pj qOSpJ)?8p=)j͠);I NIPMw FLi< ] qR@Ez(Nk6K.)dGOh~4^q7QL`Ԉ]6 qPԖΨjwq(x Բ^q(aEsrč+i߻^z VPҒP};>Mm[[!n7_j^PU<D] Q] {L| ><}U+t8 uN zbʠ*SQǖ-oI{M5N';BB3W^i:#?A~B{O z=By_ '}(Hb(ɹuf@PYt;Ȏr-~}lA)V C3l4(#]4$>4`~>%܂Z@ΉPKPwA15 ojYH!/ gS{F7 ˬ}թm4ys*7VR$hۂb*t5qQ ȔTf.|H/=3*ΐq+=?*vB>={sɶ۳3DA'zf8޻yTdtYPJntxEЙ" bڗQ[A"Sq2aЩsؘHta?C9lC> 'd~rcCSaؐg+֗'@1'?5 bOe=B{y=`B<'@gx Ma@)xP$*puzBŬrI;Xr1ZɳގuXYZ&OVv2L"ik<0 ]$&rVI3,}Ɏ/IENDB`sv607c14-2 6 71 195-643467 7428 366-6-39815l-264330.2266842ʏd8`Iy:!D$"I? RYN]J`@N﬿Q?.YC~Su&k mMG ꎩ ysZz9Ì ~e[x%-f‰УU}T0w5%9qQ2C@TCCC|p 8c 2 |(Vpai*0É!L!O8Β`dxp+-FuZF>B^<*XF 93J2b."],gV{l`M 0SI=k" I*31rUdjb&pb9?Mh0#aJuorEo(?$چ[ 2N0iQ:.Aŗ}F⿷ӳ햑ćOx(MB `wB1B$6;R1"DPD8#D+obsQ;  K! pj@ 21Ȉ,lʟIjoh1`p_6 H+NcCIy@3c KAـȰ8@jl9o2pEHf(3 PŌd _BQnhp2ৄl@mǀ}jա g~̙ۛ؛TZ';~" 5 C,^^ Khkj,^d*K!vO#,Tj0mɃvFntuY&tkhG" ~J*HCýOgiď~M^i,VL:(E XO*_E[+ _GYf LoHXJ debian-binary 1736033904 0 0 100644 4 ` 2.0 control.tar.xz 1736033904 0 0 100644 1060 ` 7zXZִFP!9'] }J>y&^ 9Nؤ猞9m_mI+*2\l@ F"" KoGU+o$/(0!"40B!cA.i.d,K) S@IVڃ~Mhď=0vN"[$"V8]yE:/jܝM}Z@7%%:, P{XvmaAnD=Suri1ѳ,b ~zר^ [S4T$P"эi\E6?0b8"J? fagr\ Xy1Y; 1S(vrIM30g#_W Ax a[̩SG pKm3ΊlEz&^Dz3GeekuDBn!7bA;'g9 bƑ^:yCo-?:-% eTO',< ݗX^)=ޢ8a 0nC%>T3ΠT梍~4˲2)-I{!3;+NON\rmɥQhټbO/ש ;ȝ=e!RA(kfrU:pWZ&;y^H2XY^ZoӴnmy]T3!'l _LewfD(y0`=]p-w Ǡ_#%+MrUU4i h+<,&v\S2Z^H _Pcqd]d5%Yo]U֖I$(6 'E9WxJx5p6W B<ԃG W?tVPfu6NsVd!ZE0k ˆ5-zƸ2oK*nP#/A,k%I`ݦړϞ" p(oex] $- M^`Ă 1J92DhP;TgYZdata.tar.xz 1736033904 0 0 100644 8356 ` 7zXZִF@! [] }J>y&^ 9Nؤ猞9m_mI+*2\l@APOl:½+X䶬:)!V5SYX<B3:F9hPX){P'E< `s*-uA5XڀTz+|t= a0H@|Ql<somVf95o^X CIePw%q2=;utSPqآ}.=}8ug L^}Lc3^+H޷Ӱ;&5,L=NRdas!x9Ǫ!Iw/d=TҥH6N_a^ѺaH ogki,}--ߦkNMnERѢ\ˊSƊ9@N>Ez |4[g 0*F>ʟaTd Oa\3x}<؞ԙcqL} TM pEy$Hc\ÌϓRyoM:HRb)e?*+ĚwE@}k.cl y)¬L{&6PͿ8Iwi;T!óȰ Ym!͙48Rkw< ѪJ)Ƈgoe(R CcSk)c^4?́"vq( 1Y*0oTue6x~ ~hVgSV/P{Fw]zSc7Eߐ}FCeuB9 䔒)8W)M.dI`9qq/YVX.@D A>C@ڣS}aϒ.M`kO1Rc ,^p5#]iGM瀉_r_}u%fm2pL6W4)]-S319]cR5W&0; [o0 RrVLaꀥE!c l>B0ԴƽrpVNXz14WMٖY !tܙ cGUb%T[8>3<<#\`Tg,S(=A:`,EWB"\tZ^| =5w~&!ׂgWǂEyxQ(ТF )/X.2ꌰE0j~–bJ4S«r?`O!r&h8`a~WwWܣ(i K~D{3g7IM/W7fFlJ?ĘySv]sa_3]rI!|Άri0imG<JJTov0٬7H?ɑ2O>s,>\J1x6vy8B(=:@tqezkeQD7Lƾ^W T<~=ȕ=![HB')dHO$@簩xD [T {o+4aD±si4%1U a?v틀Gc4E -;J,GpYץBȅw}r\dWԉݡH(Vpya>y$,cKjsپHZt)!rqUj5caF*<ג;;<@%?czi?9T j#X)=)ʼnjA!#[) 0E09԰ݬվum5Hk{m]E`42S(S$I=NY?f]) S>,uD%_݈[+7zWt@$xvȤgXݠSVdžSxio;Aćaa 1LR'>:2Tf-DȈ3r?]:;98"KVk+5 UiX?% 6` R0O5\?ZW!Y tQ6973l& z'9bw4 "c cEw "j&q,,r/Ř]}b<(|JubyO*4}wzi\У$IGxr'Tb1F+UKzp7536Ie°-s{`JU˒}FBȷ QC+ ᩷j c(:)vrF<} $%'Ղ7񀍊^tr)ZϟIgas0(Frԉqox3p3Z$|]T0X$}}!M` E?uW3L_&O7EW٨! P>6py;OA e"oc| !USbCW{p~ʧqwzez5Lw2QM*"PE7ŗ`y]n')yzRzm`r˷sJ0[Q:il7#hTP/&=R6U|NbI2z11/grmŕ'dّ&$)o9CݘWyLC8f2BJۂXrgT5PqJ9 /WQl6D4bdQ MٿH>6₭D.>kk7~;mWڄ R.e&5:nMa/k \cO<8犎e1!޳|}y3 a!yQM1S#2dFaC 6Y'@)Ov΄:t1JFL0os"柏=&}mQnz)=gpcr]G-mN;3;jª] +/\Fn*@t“U%!>(+/)IIŎ3*`Z=Ia'˕05d!2pdM͠VbB|@hpůQل=*S >֓!3ڽKP.o+G,۟ F7vX ӄo[ 1T}si>ǂeP2i`SمB*zSRR]s<ߟfҝ<1] ->^>YhUk0?:J0Uԯ|/X AS,Q.L_Ubjl77),N|kl#_0h 尐c77=5ʾлSb!cqwʫRFi/R}bd+Rnb;7W>~F)a5 NQ/",.m7dz ^!ܼ0dܧc2z s|TT#UҁYV^jEoWqw:$E4'L Ms-P1(Bԇ2&5zt$(RS-n1x '6:+1}VY{0v|:ՌQ TLbD;82'; Y1X)h8ye(Ϭ%z/E ߆k{RwȚp.k :oIge?P>╵*~gynhp fϝ)_Ye`չLGD1l$:LO0fL?DAkp`[/MTڈs֎cwckwߖʥ} Ӷ̬K\873IMGfu9Q\דr悫 <'N yS' BEAx"Nl-?LҔ\ $PN/ f[$Yz$r4Zkgr''orGH 70!S"d?ckHk`,d=dCy476vjeZ=u4 ͂aJ v2d_/E['_߹V5U@:ŝO5Գod!=hO|Ñ!:kຟF`luG'r# GG!vaA ̏ل&_X"ck/O_W?=9bpk`Әgc1VZu.Nq2 ܛcticgA=;Bl\/b@cx~m˓-F-Mfpowʸd]I^vw1^q}l<[ 8}Ͷ]k|,߂ad E'XO*:e0D_X5> ԟGt5iAGw;"S,w59NݗKc:qDS{_#݈)[q NC׃cSMg_7kBQji>CSc]õΣE]=ۏvD;sxW0^'d-5;#e^qTC0lFVn9l3 ZXJR 1 uGҧ$#4`»̏ukDH@D?&`lL6H]m4qM1R.vS-B{ii;eʬ: \~Zn^y pѨ^J\QX揋Ecp\z?I̳ 7lM#v_ f)M16qЄ0"UgJ-$1[,+AHM'dOO Gur]~N%k.;}OBLj33 uvtF/ʈD!WmxӤ/bxʶ:k͓ʜ0c9P?Nx k6}lk(*U"9^Jc*3k~Ƒ"2S ^,&tLΚt,Z+M "UMe$^#tYjs}E O8 edL.xy!LZ sGKVNSBMU)kд*sچRXl4+ }CFaRÀ^"8i6..X]Λ6i4Jwr՛i23/)b.Lao…4#>\&??R5ifc?*+_N,oCj}Fa'rh%є\!!dEJ`ql H}<Yg-|h~TFFnYcKv`#{LM(%!vcu5HUzؑOS7Fv0bssRx}=dϽCMNԻCd!)L>bd0!,Y'5{aHKXHQ}ZhDwE"cZycU b2:j: }hP 'X #>(ؠWj&dmV[k~]PEQރ>zK,2a£7G,8dfеCpL%<𖘞F)_u3تw2n-j`e[f&ձy/.DC]O\2ڠ!]uB{4XnJbH>ǹVfv[QVIT 1/'- u?J,x=8oV7V;2)&lx}'xq&5*_I9D%yA7 >\!fH2߽K0{̊Z iaeCFa_Vd&BW鷴{GvRL1z>t}/Kʥ͡~w¯fGEYJ^FCk. ui"iY4͚Չ%B[C ͼS2'fsEkh_8w?^pWD`!~#<$@e1gYZrepro-threshold-0.1.2/test_data/librust-as-slice-dev_0.2.1-1+b2_amd64.in-toto.link000064400000000000000000000021471046102023000252740ustar 00000000000000{"signatures":[{"keyid":"70a3c0fb49aaae5e4944ded6761ede4e09f9f1ea0a08e01630b59e119963c5c5","sig":"c31436116149a0661b3ffcbb795c8ac56bcf194c1eed2c7b3af9e64a0d34d490210e2b2a06d83f9ee0978ddd1cc43ea18cc584ceeb0d71a78ee8f5987af1f806"},{"keyid":"2980aa6249ad89ce0ef0f0f5e6d0896214301b2818aad33443444cf460df6ed6","sig":"51812e8d9714e95f0711f758fd3da78737e73abef6fc2e27a4d618963043f50d4db7982bd6f3cb0c870b7798802a3ce5cf0e7d11e6b8f530926ad65c66386a01"}],"signed":{"_type":"link","name":"rebuild librust-as-slice-dev_0.2.1-1+b2_amd64.deb","materials":{"rust-as-slice_0.2.1-1+b2_amd64.buildinfo":{"sha256":"1b6fd8b8f0d6cf1f5e4923fac390b671619ddeb58a34656040da86a657ac72f4","sha512":"0dcbd43e2bf43d61e41c9ffb3935bdcba141c744ee2657a9d5ba4217ac6564c857fe81683ce970a2beaaeb1e0cb2d03bc707d97f81986184a0cdfe8334133956"}},"products":{"librust-as-slice-dev_0.2.1-1+b2_amd64.deb":{"sha512":"98c17aafed2f8043af096570064cb2f9da1788822077cde7e580b6014f5271000d7b2785cde6144e750f6a200a2641c1072465c3df2679e41672b739912b00c2","sha256":"32be954941cdb42bce44c19ece910af04ece3a1acc9b79fa3e8ff735ffb511cd"}},"environment":null,"byproducts":{},"command":[]}}repro-threshold-0.1.2/test_data/reproduce-debian-net-amd64.pub000064400000000000000000000001651046102023000223340ustar 00000000000000-----BEGIN PUBLIC KEY----- MCwwBwYDK2VwBQADIQATuimKMfCOSfiKc2AT/T1NKQhn9y39Kz5Suo0w5UUXGA== -----END PUBLIC KEY----- repro-threshold-0.1.2/test_data/reproducible-archlinux.pub000064400000000000000000000001701046102023000220750ustar 00000000000000-----BEGIN PUBLIC KEY----- MCwwBwYDK2VwBQADIQBLNcEcgErQ1rZz9oIkUnzc3fPuqJEALr22rNbrBK7iqQ== -----END PUBLIC KEY-----