diff --git a/CHANGELOG.md b/CHANGELOG.md index 46bfa459..d236aa24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ The latest `## [X.Y.Z]` header must match the `version` field in `punkproject.to Entries are newest-first; one bullet per notable change. See the root `AGENTS.md` "Project Versioning" section for the bump policy. +## [0.28.4] - 2026-07-27 + +- punk::zip 0.3.0 (G-126): optional punkzip extraction accelerator. New + `punk::zip::accelerator` (auto|none|; auto probes env(PUNKZIP_EXE) then a + punkzip beside the executable, resolution cached); `unzip` hands whole-archive member + writing to the binary when the call is one it serves identically (whole archive, + default -overwrite/-mtime/-verify, ascii names) and re-stamps mtimes to punk::zip's + local-time convention afterwards, falling back to pure Tcl silently otherwise + (punk::zip::last_unzip_engine / last_accelerator_note record the decision). Preflight + refusals, member selection and returned names always come from punk::zip's own + reader. Parity suite src/tests/modules/punk/zip/testsuites/zip/zipaccel.test proves + identical names, bytes and mtimes through both engines over the G-124 shapes and the + real file-relative kit runtime. Measured: material from ~50 members; x2.3-2.6 at kit + scale under a native tclsh; x3.8-4.9 kit-hosted (e.g. punk91.exe 21.9s -> 5.7s). + ## [0.28.3] - 2026-07-27 - New `make.tcl tool` subcommand (G-126): surface for the vendored first-party zig diff --git a/goals/G-126-punkzip-accelerator.md b/goals/G-126-punkzip-accelerator.md index 3fb94cb9..aa0c39ea 100644 --- a/goals/G-126-punkzip-accelerator.md +++ b/goals/G-126-punkzip-accelerator.md @@ -279,6 +279,48 @@ working tree; one now exists. (user decision 2026-07-27). The acceptance's build-step clause is satisfied: bin/punkzip.exe is reproducible on a clean checkout from vendored source + pinned toolchain + this step. -- Remaining for acceptance: punk::zip accelerator detection + fast path behind the - G-124 floor; parity suite in `src/tests/modules/punk/zip/`; recorded benchmark - (speedup + entry count where it becomes material). +- 2026-07-27 punk::zip fast path + parity suite + benchmark LANDED (punkshell 0.28.4, + punk::zip 0.3.0) - the final acceptance clauses: + - `punk::zip::accelerator` (auto|none|): auto probes env(PUNKZIP_EXE) then a + punkzip beside [info nameofexecutable]; resolution cached until reconfigured. + `unzip` hands whole-archive member WRITING to the accelerator only when the call + is one it serves identically (globs {*}, no excludes, default + -overwrite/-mtime/-verify, ascii names) and re-stamps mtimes with punk::zip's + local-time convention afterwards so the engines produce identical trees; + preflight refusals, member selection and returned names always come from + punk::zip's own reader (the G-124 floor, unchanged and still sole authority); + any accelerator failure falls back to pure Tcl silently. + punk::zip::last_unzip_engine / last_accelerator_note expose the per-call + decision for tests and diagnostics. + - Parity suite `src/tests/modules/punk/zip/testsuites/zip/zipaccel.test` (9 tests, + green; accelerator-dependent cases self-gate on bin/punkzip): identical names, + bytes AND mtimes through both engines over the G-124 shapes (plain, + archive-relative, file-relative) and whole-kit extraction of the recorded + file-relative `tclsh90b4_piperepl.exe` (841 members); listings engine-independent; + porcelain listing agrees with punk::zip member dicts on every shared field + (mtime excluded from the crosscheck by design: porcelain is the tz-free UTC + interpretation, punk::zip members the local-time convention - see the timestamp + Notes bullet); pure path proven taken when the binary is absent; selective calls + proven to run pure silently. Implicit directories (kits store no dir entries) + are excluded from mtime comparison - neither engine stamps them. + - Recorded benchmark (best of 3, whole-archive `punk::zip::unzip`, this machine + 2026-07-27, bin/punkzip.exe v2.3.1 ReleaseSafe): + native Git tclsh 8.6.11: + info-zip fixture 4 members pure 4.8ms accel 13.6ms x0.35 + synthetic tiny 50 members pure 71.7ms accel 41.1ms x1.74 + synthetic tiny 200 members pure 286.9ms accel 126.4ms x2.27 + synthetic tiny 500 members pure 709.9ms accel 303.7ms x2.34 + piperepl.exe 841 members pure 1328.3ms accel 503.6ms x2.64 + punk91.exe (49MB) 3629 members pure 6670.1ms accel 2837.1ms x2.35 + kit-hosted (punk902z script, tcl 9.0.2, punk stack loaded - the bake-from-kit + scenario that motivated this goal): + piperepl.exe 841 members pure 4772.1ms accel 968.9ms x4.93 + punk91.exe 3629 members pure 21850.9ms accel 5683.5ms x3.84 + Accelerator fixed cost is ~9-13ms (process spawn + its own archive read); + crossover vs pure is ~15-20 members; it becomes MATERIAL (>= x1.7) from roughly + 50 members, and the kit-hosted case gains most (3.8-16s per kit extraction). + - Full runtests suite at recorded baseline under the native tclsh9.0.5-punk + runtime: 1186/1211 passed, 24 skipped, 1 failed = the pre-existing exec-14.3 + baseline. (The Git tclsh is not a full-suite runner - 194 environmental + failures there predate this work.) + ACCEPTANCE FULLY MET - flipped achieved 2026-07-27 as part of this closeout. diff --git a/punkproject.toml b/punkproject.toml index e80e9765..420e5c48 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,4 +1,4 @@ [project] name = "punkshell" -version = "0.28.3" +version = "0.28.4" license = "BSD-2-Clause" diff --git a/src/modules/punk/zip-999999.0a1.0.tm b/src/modules/punk/zip-999999.0a1.0.tm index 266b3ee8..467135dc 100644 --- a/src/modules/punk/zip-999999.0a1.0.tm +++ b/src/modules/punk/zip-999999.0a1.0.tm @@ -66,7 +66,15 @@ package require punk::args # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ tcl::namespace::eval punk::zip { tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase - #variable xyz + + #G-126 accelerator state - see punk::zip::accelerator and punk::zip::unzip. + #The pure-Tcl reader is the always-available floor; the vendored punkzip + #binary is an optional per-call extraction accelerator. + variable accelerator_config auto ;#auto | none | + variable accelerator_resolved "" + variable accelerator_resolved_for "\uFFFF" ;#config value the cached resolution was computed for + variable last_unzip_engine "" ;#tcl | accelerated - which engine the last unzip used + variable last_accelerator_note "" ;#why the last unzip skipped the accelerator or fell back #*** !doctools #[subsection {Namespace punk::zip}] @@ -1104,6 +1112,79 @@ tcl::namespace::eval punk::zip { return [Select_members [dict get $arc members] $globs $excludes] } + punk::args::define { + @id -id ::punk::zip::accelerator + @cmd -name punk::zip::accelerator\ + -summary\ + "Query or configure the optional punkzip extraction accelerator"\ + -help\ + "punk::zip can hand whole-archive extraction to the vendored punkzip + tool (goal G-126; built to bin/ by 'make.tcl tool build punkzip') + when a usable binary is present - materially faster for large member + counts - while the pure-Tcl reader remains the always-available + floor and the authority on preflight refusals, member selection and + returned names. + + With no argument, returns the currently resolved accelerator path, + or an empty string when none is usable. With an argument, sets the + configuration and returns the new resolution: + auto - (default) probe: env(PUNKZIP_EXE) if set, else a punkzip + executable beside [info nameofexecutable] + none - disable the accelerator (pure-Tcl always) + - use the punkzip binary at an explicit path + + Resolution is cached until the configuration changes. See + punk::zip::unzip for when the accelerator is actually used: calls it + cannot serve identically run pure-Tcl, and an accelerator failure + falls back to pure-Tcl silently. For diagnostics and tests, + punk::zip::last_unzip_engine records which engine the last unzip + used (tcl|accelerated) and punk::zip::last_accelerator_note records + why the accelerator was skipped or abandoned." + @values -min 0 -max 1 + config -optional 1 -default "" -help\ + "auto | none | path of a punkzip executable. + Empty (or omitted) queries without changing the configuration." + } + proc accelerator {args} { + set argd [punk::args::parse $args withid ::punk::zip::accelerator] + variable accelerator_config + variable accelerator_resolved + variable accelerator_resolved_for + set config [dict get $argd values config] + if {$config ne ""} { + set accelerator_config $config + } + if {$accelerator_resolved_for eq $accelerator_config} { + return $accelerator_resolved + } + set resolved "" + set candidates [list] + switch -exact -- $accelerator_config { + none {} + auto { + if {[info exists ::env(PUNKZIP_EXE)] && $::env(PUNKZIP_EXE) ne ""} { + lappend candidates $::env(PUNKZIP_EXE) + } else { + set exedir [file dirname [info nameofexecutable]] + lappend candidates [file join $exedir punkzip.exe] [file join $exedir punkzip] + } + } + default { + lappend candidates $accelerator_config + } + } + foreach c $candidates { + #file executable is unreliable for some windows setups - existence suffices there + if {[file isfile $c] && ([file executable $c] || $::tcl_platform(platform) eq "windows")} { + set resolved [file normalize $c] + break + } + } + set accelerator_resolved $resolved + set accelerator_resolved_for $accelerator_config + return $resolved + } + punk::args::define { @id -id ::punk::zip::unzip @cmd -name punk::zip::unzip\ @@ -1124,6 +1205,15 @@ tcl::namespace::eval punk::zip { naming the reason instead of leaving a half-populated directory. A member whose path would escape targetdir is refused the same way. + When the punkzip accelerator is available (see punk::zip::accelerator) + and the call is one it serves identically - whole archive, default + -overwrite/-mtime/-verify, ascii member names - the member data is + written by the accelerator instead of the pure-Tcl loop, with mtimes + re-stamped to this module's convention afterwards; preflight, member + selection and the returned names always come from punk::zip's own + reader, and any accelerator failure falls back to pure Tcl silently. + punk::zip::last_unzip_engine records which engine ran. + Returns the list of member names extracted (see -return). Examples: @@ -1178,6 +1268,11 @@ tcl::namespace::eval punk::zip { set restoremtime [dict get $argd opts -mtime] set verify [dict get $argd opts -verify] + variable last_unzip_engine + variable last_accelerator_note + set last_unzip_engine tcl + set last_accelerator_note "" + set arc [Open_archive $zipfile 1] set chan [dict get $arc chan] set extracted [list] @@ -1186,6 +1281,7 @@ tcl::namespace::eval punk::zip { set selected [Select_members [dict get $arc members] $globs $excludes] #preflight - nothing is written until every selected member is known good set targets [list] + set names_ascii 1 foreach m $selected { set reason [Member_unsupported_reason $m] if {$reason ne ""} { @@ -1195,24 +1291,71 @@ tcl::namespace::eval punk::zip { if {!$overwrite && !([dict get $m isdirectory]) && [file exists $target]} { error "punk::zip::unzip: '$target' already exists and -overwrite is 0" } + if {![string is ascii [dict get $m name]]} { + set names_ascii 0 + } lappend targets $target } file mkdir $targetdir - foreach m $selected target $targets { - set name [dict get $m name] - if {[dict get $m isdirectory]} { - file mkdir $target - if {$restoremtime && [dict get $m mtime]} { - lappend dirtimes $target [dict get $m mtime] - } + + #G-126 accelerator: hand whole-archive member WRITING to the vendored + #punkzip binary when this call is one it serves identically. punk::zip's + #own parse above stays authoritative for preflight refusals, member + #selection and the returned names; the pure-Tcl loop below is the + #always-available floor and any accelerator failure falls back to it + #silently. Eligibility: whole archive (globs {*}, no excludes), the + #default -overwrite/-mtime/-verify semantics punkzip matches (it always + #overwrites, restores times and crc-verifies), and ascii member names + #(punkzip's name handling is wtf-8; punk::zip decodes cp437/utf-8 flags). + #punkzip stamps mtimes with its tz-free utc convention, so after a + #successful run the members are re-stamped below with this module's + #local-time convention - the two engines produce identical trees. + set accelerated 0 + if {$verify && $overwrite && $restoremtime && $names_ascii + && [llength $excludes] == 0 && [llength $globs] == 1 && [lindex $globs 0] eq "*"} { + set acc [accelerator] + if {$acc eq ""} { + set last_accelerator_note "no accelerator binary resolved (see punk::zip::accelerator)" + } elseif {[catch {exec $acc extract -d $targetdir $zipfile 2>@1} accout]} { + set last_accelerator_note "accelerator '$acc' failed - fell back to pure tcl: [string range $accout 0 300]" } else { - file mkdir [file dirname $target] - Extract_member $chan $m $target $verify - if {$restoremtime && [dict get $m mtime]} { - catch {file mtime $target [dict get $m mtime]} + set accelerated 1 + } + } else { + set last_accelerator_note "call not accelerator-eligible (selective, non-default options, or non-ascii names) - pure tcl" + } + + if {$accelerated} { + set last_unzip_engine accelerated + foreach m $selected target $targets { + if {[dict get $m isdirectory]} { + if {[dict get $m mtime]} { + lappend dirtimes $target [dict get $m mtime] + } + } else { + if {[dict get $m mtime]} { + catch {file mtime $target [dict get $m mtime]} + } + } + lappend extracted [dict get $m name] + } + } else { + foreach m $selected target $targets { + set name [dict get $m name] + if {[dict get $m isdirectory]} { + file mkdir $target + if {$restoremtime && [dict get $m mtime]} { + lappend dirtimes $target [dict get $m mtime] + } + } else { + file mkdir [file dirname $target] + Extract_member $chan $m $target $verify + if {$restoremtime && [dict get $m mtime]} { + catch {file mtime $target [dict get $m mtime]} + } } + lappend extracted $name } - lappend extracted $name } } finally { close $chan diff --git a/src/modules/punk/zip-buildversion.txt b/src/modules/punk/zip-buildversion.txt index 0a6e799d..b66090e8 100644 --- a/src/modules/punk/zip-buildversion.txt +++ b/src/modules/punk/zip-buildversion.txt @@ -1,6 +1,17 @@ -0.2.0 +0.3.0 #First line must be a semantic version number #all other lines are ignored. +#0.3.0 - optional punkzip extraction accelerator (G-126): new punk::zip::accelerator +# (auto|none|; auto probes env(PUNKZIP_EXE) then a punkzip beside +# [info nameofexecutable], resolution cached). unzip hands whole-archive member +# WRITING to the accelerator when the call is one it serves identically (globs {*}, +# no excludes, default -overwrite/-mtime/-verify, ascii names) and re-stamps mtimes +# with this module's local-time convention afterwards so the engines produce +# identical trees; preflight refusals, member selection and returned names always +# come from punk::zip's own reader, and accelerator failure falls back to pure Tcl +# silently. Diagnostics: punk::zip::last_unzip_engine (tcl|accelerated) and +# punk::zip::last_accelerator_note. Parity suite: +# src/tests/modules/punk/zip/testsuites/zip/zipaccel.test. #0.2.0 - dependency-free reading (G-124): archive_info/members/unzip read a plain zip or an # executable-prefixed archive under either offset convention using stock Tcl only - no # zipfs, no vfs::zip, no tcllib. Base-offset derivation factored out of extract_preamble diff --git a/src/tests/modules/punk/zip/testsuites/zip/zipaccel.test b/src/tests/modules/punk/zip/testsuites/zip/zipaccel.test new file mode 100644 index 00000000..df9a5c77 --- /dev/null +++ b/src/tests/modules/punk/zip/testsuites/zip/zipaccel.test @@ -0,0 +1,271 @@ +package require tcltest + +package require punk::zip + +#added 2026-07-27 (agent, G-126) - parity suite for the punkzip extraction accelerator. +#Proves the accelerated and pure-Tcl unzip paths produce IDENTICAL results (member name +#lists, byte-identical extraction trees, matching file AND directory mtimes) over the +#G-124 fixture shapes (plain / archive-relative prefixed / file-relative prefixed, one +#source tree), that listings are engine-independent, that punkzip's porcelain listing +#agrees with punk::zip's member dicts on every shared field, that the pure-Tcl path is +#taken when the accelerator binary is absent, and that calls the accelerator cannot +#serve identically (selective globs) silently run pure-Tcl. +#The accelerator-dependent tests self-gate on bin/punkzip existing ('make.tcl tool +#build punkzip' builds it); the pure-path-when-absent proof runs everywhere. +#mtime NOTE: porcelain mtimes are punkzip's tz-free UTC interpretation of the DOS +#fields while punk::zip's member mtimes use the local-time convention, so the +#porcelain crosscheck compares every shared field EXCEPT mtime (recorded in the +#G-126 goal notes); extraction-tree mtimes ARE compared - the accelerated path +#re-stamps them with punk::zip's convention, so the engines must agree exactly. + +namespace eval ::testspace { + namespace import ::tcltest::* + + variable BASE [makeDirectory g126_zipaccel] + + variable projectroot [file normalize [file join [file dirname [info script]] .. .. .. .. .. .. ..]] + variable ACCEL "" + foreach _cand [list [file join $projectroot bin punkzip.exe] [file join $projectroot bin punkzip]] { + if {[file isfile $_cand]} { + set ACCEL $_cand + break + } + } + testConstraint punkzipavailable [expr {$ACCEL ne ""}] + + #the recorded file-relative runtime named in the G-126 Acceptance (same pin as + #zipreader.test). Not in the repo (bin/ build outputs are untracked) so it self-gates. + variable PIPEREPL [file join $projectroot bin runtime win32-x86_64 tclsh90b4_piperepl.exe] + testConstraint piperepl_runtime [file exists $PIPEREPL] + + proc fwrite {path content} { + file mkdir [file dirname $path] + set fd [open $path wb] + puts -nonewline $fd $content + close $fd + } + proc fread {path} { + set fd [open $path rb] + set data [read $fd] + close $fd + return $data + } + + #paths whose mtimes the engines actually STAMP: members with a representable + #stored time. Anything else (implicit intermediate directories - e.g. the + #piperepl kit stores no directory entries at all - or zero-mtime members) gets + #extraction wall-time from either engine, which legitimately differs between + #two runs and must not be compared. + proc stampset {archive} { + set s [dict create] + foreach m [punk::zip::members $archive] { + if {[dict get $m mtime]} { + dict set s [string trimright [dict get $m name] /] 1 + } + } + return $s + } + #recursive snapshot: relpath -> dir/file record incl. content crc and mtime + #(mtime recorded as 'unstamped' for paths outside the stamped set) + proc treesnap {base stamped {rel ""}} { + set snap [dict create] + set dir [expr {$rel eq "" ? $base : [file join $base $rel]}] + foreach p [lsort [glob -nocomplain -dir $dir *]] { + set name [file tail $p] + set r [expr {$rel eq "" ? $name : "$rel/$name"}] + set mt [expr {[dict exists $stamped $r] ? [file mtime $p] : "unstamped"}] + if {[file isdirectory $p]} { + dict set snap $r [list dir mtime $mt] + set snap [dict merge $snap [treesnap $base $stamped $r]] + } else { + dict set snap $r [list file size [file size $p] crc [format %08x [zlib crc32 [fread $p]]] mtime $mt] + } + } + return $snap + } + #empty string when identical; otherwise names the first difference + proc snapdiff {a b} { + set ka [lsort [dict keys $a]] + set kb [lsort [dict keys $b]] + if {$ka ne $kb} { + set onlya [list] + set onlyb [list] + foreach k $ka {if {![dict exists $b $k]} {lappend onlya $k}} + foreach k $kb {if {![dict exists $a $k]} {lappend onlyb $k}} + return "keysets differ - only-first:$onlya only-second:$onlyb" + } + dict for {k v} $a { + if {$v ne [dict get $b $k]} { + return "entry '$k' differs: $v vs [dict get $b $k]" + } + } + return "" + } + + # -- --- --- fixtures: the G-124 shapes (one source tree, packed three ways) --- --- -- + variable SRC [file join $BASE srctree] + variable PREAMBLE [file join $BASE preamble.bin] + variable PLAIN [file join $BASE plain.zip] + variable ARCREL [file join $BASE arcrel.bin] + variable FILEREL [file join $BASE filerel.bin] + + file delete -force $SRC + file mkdir $SRC/sub/deeper + fwrite $SRC/hello.txt "hello world\n" + fwrite $SRC/sub/nested.tcl "puts nested\n" + set fixturebytes "" + for {set i 0} {$i < 256} {incr i} { + append fixturebytes [binary format c $i] + } + fwrite $SRC/binary.dat $fixturebytes + set BIGTEXT "" + for {set i 0} {$i < 40000} {incr i} { + append BIGTEXT "line $i of a member large enough to force the streaming path\n" + } + fwrite $SRC/sub/deeper/big.txt $BIGTEXT + fwrite $PREAMBLE [string repeat "PREAMBLE-BYTES-\x00\x01\x02\x50\x4b\x05\x06" 4096] + punk::zip::mkzip -return none -directory $SRC -- $PLAIN * + punk::zip::mkzip -return none -offsettype archive -runtime $PREAMBLE -directory $SRC -- $ARCREL * + punk::zip::mkzip -return none -offsettype file -runtime $PREAMBLE -directory $SRC -- $FILEREL * + + #run one extraction under an explicit accelerator configuration; returns a dict + #with the extracted names, the engine that ran, the fallback note and a snapshot + proc extract_with {config archive outdir args} { + variable BASE + set target [file join $BASE $outdir] + file delete -force $target + punk::zip::accelerator $config + set names [punk::zip::unzip $archive $target {*}$args] + set r [dict create\ + names [lsort $names]\ + engine $::punk::zip::last_unzip_engine\ + note $::punk::zip::last_accelerator_note\ + snap [treesnap $target [stampset $archive]]\ + ] + punk::zip::accelerator auto + return $r + } + + # -- --- --- accelerator resolution --- --- -- + + test zipaccel_resolves_explicit_path {an explicit accelerator path resolves and none disables} -constraints {punkzipavailable} -body { + variable ACCEL + set result [list] + lappend result explicit [expr {[punk::zip::accelerator $ACCEL] ne ""}] + lappend result none [punk::zip::accelerator none] + punk::zip::accelerator auto + set result + } -result {explicit 1 none {}} + + test zipaccel_pure_path_when_absent {a missing accelerator binary resolves empty and unzip runs pure-Tcl successfully} -body { + variable PLAIN + set r [extract_with [file join $::testspace::BASE nosuch punkzip.exe] $PLAIN out_absent] + list resolved_empty [expr {$::punk::zip::accelerator_resolved eq ""}]\ + engine [dict get $r engine]\ + extracted_count [llength [dict get $r names]]\ + note_names_it [string match "*no accelerator binary resolved*" [dict get $r note]] + } -result {resolved_empty 1 engine tcl extracted_count 6 note_names_it 1} + + # -- --- --- listings are engine-independent --- --- -- + + test zipaccel_listing_engine_independent {members and archive_info return identical results whatever the accelerator configuration} -constraints {punkzipavailable} -body { + variable PLAIN + variable ACCEL + punk::zip::accelerator none + set m1 [punk::zip::members $PLAIN] + set i1 [punk::zip::archive_info $PLAIN] + punk::zip::accelerator $ACCEL + set m2 [punk::zip::members $PLAIN] + set i2 [punk::zip::archive_info $PLAIN] + punk::zip::accelerator auto + list members_equal [expr {$m1 eq $m2}] info_equal [expr {$i1 eq $i2}] + } -result {members_equal 1 info_equal 1} + + # -- --- --- porcelain crosscheck: two independent readers agree --- --- -- + + test zipaccel_porcelain_crosscheck {punkzip's porcelain listing agrees with punk::zip's member dicts on every shared field} -constraints {punkzipavailable} -body { + variable PLAIN + variable ACCEL + set out [exec $ACCEL list -porcelain $PLAIN 2>@1] + set porc [dict create] + set sawversion 0 + foreach line [split $out \n] { + set line [string trimright $line \r] + if {$line eq "" || [string range $line 0 1] eq "# "} {continue} + if {!$sawversion} { + if {$line eq "punkzip_porcelain_v1"} {set sawversion 1} + continue + } + if {[regexp {^m (\d) (\S+) (\d+) (\d+) ([0-9a-f]{8}) (-?\d+) (\d) (\d+) (\d+) (.*)$} $line -> isdir methodname uncomp comp crc mtime enc madeby namelen name]} { + dict set porc $name [dict create isdir $isdir methodname $methodname size $uncomp csize $comp crc $crc encrypted $enc madeby $madeby namelen $namelen] + } + } + set result [list sawversion $sawversion] + set members [punk::zip::members $PLAIN] + lappend result member_count_equal [expr {[dict size $porc] == [llength $members]}] + set mismatches [list] + foreach m $members { + set name [dict get $m name] + if {![dict exists $porc $name]} { + lappend mismatches "missing-from-porcelain:$name" + continue + } + set p [dict get $porc $name] + foreach {pkey mkey conv} {isdir isdirectory {} methodname methodname {} size size {} csize csize {} encrypted encrypted {} madeby madeby {}} { + set mv [dict get $m $mkey] + if {[dict get $p $pkey] ne $mv} { + lappend mismatches "$name:$pkey=[dict get $p $pkey]!=$mv" + } + } + if {[dict get $p crc] ne [format %08x [dict get $m crc]]} { + lappend mismatches "$name:crc" + } + if {[dict get $p namelen] != [string length $name]} { + lappend mismatches "$name:namelen" + } + } + lappend result mismatches $mismatches + set result + } -result {sawversion 1 member_count_equal 1 mismatches {}} + + # -- --- --- extraction parity over the three shapes --- --- -- + + foreach {shapename shapevar} {plain PLAIN archiverelative ARCREL filerelative FILEREL} { + test zipaccel_unzip_parity_$shapename "accelerated and pure-Tcl extraction of the $shapename shape produce identical names, bytes and mtimes" -constraints {punkzipavailable} -body [string map [list %V% $shapevar] { + variable %V% + variable ACCEL + set pure [extract_with none [set %V%] out_pure] + set fast [extract_with $ACCEL [set %V%] out_fast] + list engines [list [dict get $pure engine] [dict get $fast engine]]\ + names_equal [expr {[dict get $pure names] eq [dict get $fast names]}]\ + treediff [snapdiff [dict get $pure snap] [dict get $fast snap]] + }] -result {engines {tcl accelerated} names_equal 1 treediff {}} + } + + # -- --- --- per-call decision: ineligible calls silently run pure --- --- -- + + test zipaccel_selective_call_runs_pure {a selective (glob) extraction is not accelerator-eligible and silently runs pure-Tcl} -constraints {punkzipavailable} -body { + variable PLAIN + variable ACCEL + set r [extract_with $ACCEL $PLAIN out_selective sub/*] + list engine [dict get $r engine]\ + names [dict get $r names]\ + note_names_it [string match "*not accelerator-eligible*" [dict get $r note]] + } -result {engine tcl names {sub/ sub/deeper/ sub/deeper/big.txt sub/nested.tcl} note_names_it 1} + + # -- --- --- the real file-relative prefixed executable --- --- -- + + test zipaccel_piperepl_parity {whole-kit extraction of the recorded file-relative runtime is identical through both engines} -constraints {punkzipavailable piperepl_runtime} -body { + variable PIPEREPL + variable ACCEL + set pure [extract_with none $PIPEREPL out_rt_pure] + set fast [extract_with $ACCEL $PIPEREPL out_rt_fast] + list engines [list [dict get $pure engine] [dict get $fast engine]]\ + member_count [llength [dict get $pure names]]\ + names_equal [expr {[dict get $pure names] eq [dict get $fast names]}]\ + treediff [snapdiff [dict get $pure snap] [dict get $fast snap]] + } -result {engines {tcl accelerated} member_count 841 names_equal 1 treediff {}} + + cleanupTests +} +namespace delete ::testspace