diff --git a/src/bootsupport/lib/vfszip/pkgIndex.tcl b/src/bootsupport/lib/vfszip/pkgIndex.tcl new file mode 100644 index 00000000..60421d79 --- /dev/null +++ b/src/bootsupport/lib/vfszip/pkgIndex.tcl @@ -0,0 +1,53 @@ +# -*- tcl -*- +# Tcl package index file, version 1.1 +# This file was generated by hand. +# +# This will be autogenerated by configure to use the correct name +# for the vfs dynamic library. + +#package ifneeded vfs 1.5.0 [list source [file join $dir vfs.tcl]] +# +#package ifneeded starkit 1.3.3 [list source [file join $dir starkit.tcl]] +# +## New, for the old, keep version numbers synchronized. +#package ifneeded vfs::mk4 1.10.1 [list source [file join $dir mk4vfs.tcl]] + + +#2025 - provide a fix for 'bad central header' error in zip::open when platform has older vfs library +package ifneeded vfs::zip 1.0.4 [list source [file join $dir zipvfs.tcl]] + +# New +#package ifneeded vfs::ftp 1.0 [list source [file join $dir ftpvfs.tcl]] +#package ifneeded vfs::http 0.6 [list source [file join $dir httpvfs.tcl]] +#package ifneeded vfs::ns 0.5.1 [list source [file join $dir tclprocvfs.tcl]] +#package ifneeded vfs::tar 0.91 [list source [file join $dir tarvfs.tcl]] +#package ifneeded vfs::test 1.0 [list source [file join $dir testvfs.tcl]] +#package ifneeded vfs::urltype 1.0 [list source [file join $dir vfsUrl.tcl]] +#package ifneeded vfs::webdav 0.1 [list source [file join $dir webdavvfs.tcl]] +#package ifneeded vfs::tk 0.5 [list source [file join $dir tkvfs.tcl]] +## +## Virtual filesystems based on the template vfs: +## +#if {[lsearch -exact $::auto_path [file join $dir template]] < 0} { +# lappend ::auto_path [file join $dir template] +#} +#package ifneeded vfs::template::chroot 1.5.2 \ +# [list source [file join $dir template chrootvfs.tcl]] +#package ifneeded vfs::template::collate 1.5.3 \ +# [list source [file join $dir template collatevfs.tcl]] +#package ifneeded vfs::template::version 1.5.2 \ +# [list source [file join $dir template versionvfs.tcl]] +#package ifneeded vfs::template::version::delta 1.5.2 \ +# [list source [file join $dir template deltavfs.tcl]] +#package ifneeded vfs::template::fish 1.5.2 \ +# [list source [file join $dir template fishvfs.tcl]] +#package ifneeded vfs::template::quota 1.5.2 \ +# [list source [file join $dir template quotavfs.tcl]] +#package ifneeded vfs::template 1.5.5 \ +# [list source [file join $dir template templatevfs.tcl]] +## +## Helpers +## +#package ifneeded fileutil::globfind 1.5 \ +# [list source [file join $dir template globfind.tcl]] +#package ifneeded trsync 1.0 [list source [file join $dir template tdelta.tcl]] diff --git a/src/bootsupport/lib/vfszip/zipvfs.tcl b/src/bootsupport/lib/vfszip/zipvfs.tcl new file mode 100644 index 00000000..0a0ef767 --- /dev/null +++ b/src/bootsupport/lib/vfszip/zipvfs.tcl @@ -0,0 +1,937 @@ +# Removed provision of the backward compatible name. Moved to separate +# file/package. +package provide vfs::zip 1.0.4 + +package require vfs + +# Using the vfs, memchan and Trf extensions, we ought to be able +# to write a Tcl-only zip virtual filesystem. What we have below +# is basically that. + +namespace eval vfs::zip {} + +# Used to execute a zip archive. This is rather like a jar file +# but simpler. We simply mount it and then source a toplevel +# file called 'main.tcl'. +proc vfs::zip::Execute {zipfile} { + Mount $zipfile $zipfile + source [file join $zipfile main.tcl] +} + +proc vfs::zip::Mount {zipfile local} { + set fd [::zip::open [::file normalize $zipfile]] + vfs::filesystem mount $local [list ::vfs::zip::handler $fd] + # Register command to unmount + vfs::RegisterMount $local [list ::vfs::zip::Unmount $fd] + return $fd +} + +proc vfs::zip::Unmount {fd local} { + vfs::filesystem unmount $local + ::zip::_close $fd +} + +proc vfs::zip::handler {zipfd cmd root relative actualpath args} { + #::vfs::log [list $zipfd $cmd $root $relative $actualpath $args] + if {$cmd == "matchindirectory"} { + eval [list $cmd $zipfd $relative $actualpath] $args + } else { + eval [list $cmd $zipfd $relative] $args + } +} + +proc vfs::zip::attributes {zipfd} { return [list "state"] } +proc vfs::zip::state {zipfd args} { + vfs::attributeCantConfigure "state" "readonly" $args +} + +# If we implement the commands below, we will have a perfect +# virtual file system for zip files. + +proc vfs::zip::matchindirectory {zipfd path actualpath pattern type} { + #::vfs::log [list matchindirectory $path $actualpath $pattern $type] + + # This call to zip::getdir handles empty patterns properly as asking + # for the existence of a single file $path only + set res [::zip::getdir $zipfd $path $pattern] + #::vfs::log "got $res" + if {![string length $pattern]} { + if {![::zip::exists $zipfd $path]} { return {} } + set res [list $actualpath] + set actualpath "" + } + + set newres [list] + foreach p [::vfs::matchCorrectTypes $type $res $actualpath] { + lappend newres [file join $actualpath $p] + } + #::vfs::log "got $newres" + return $newres +} + +proc vfs::zip::stat {zipfd name} { + #::vfs::log "stat $name" + ::zip::stat $zipfd $name sb + #::vfs::log [array get sb] + # remove socket mode file type (0xc000) to prevent Tcl from reporting Fossil archives as socket types + if {($sb(mode) & 0xf000) == 0xc000} { + set sb(mode) [expr {$sb(mode) ^ 0xc000}] + } + # remove block device bit file type (0x6000) + if {($sb(mode) & 0xf000) == 0x6000} { + set sb(mode) [expr {$sb(mode) ^ 0x6000}] + } + # remove FIFO mode file type (0x1000) + if {($sb(mode) & 0xf000) == 0x1000} { + set sb(mode) [expr {$sb(mode) ^ 0x1000}] + } + # remove character device mode file type (0x2000) + if {($sb(mode) & 0xf000) == 0x2000} { + set sb(mode) [expr {$sb(mode) ^ 0x2000}] + } + # workaround for certain errorneus zip archives + if {($sb(mode) & 0xffff) == 0xffff} { + # change to directory type and set mode to 0777 + directory flag + set sb(mode) 0x41ff + } + array get sb +} + +proc vfs::zip::access {zipfd name mode} { + #::vfs::log "zip-access $name $mode" + if {$mode & 2} { + vfs::filesystem posixerror $::vfs::posix(EROFS) + } + # Readable, Exists and Executable are treated as 'exists' + # Could we get more information from the archive? + if {[::zip::exists $zipfd $name]} { + return 1 + } else { + error "No such file" + } + +} + +proc vfs::zip::open {zipfd name mode permissions} { + #::vfs::log "open $name $mode $permissions" + # return a list of two elements: + # 1. first element is the Tcl channel name which has been opened + # 2. second element (optional) is a command to evaluate when + # the channel is closed. + + switch -- $mode { + "" - + "r" { + if {![::zip::exists $zipfd $name]} { + vfs::filesystem posixerror $::vfs::posix(ENOENT) + } + + ::zip::stat $zipfd $name sb + + if {$sb(ino) < 0} { + vfs::filesystem posixerror $::vfs::posix(EISDIR) + } + +# set nfd [vfs::memchan] +# fconfigure $nfd -translation binary + + seek $zipfd $sb(ino) start +# set data [zip::Data $zipfd sb 0] + +# puts -nonewline $nfd $data + +# fconfigure $nfd -translation auto +# seek $nfd 0 +# return [list $nfd] + # use streaming for files larger than 1MB + if {$::zip::useStreaming && $sb(size) >= 1048576} { + seek $zipfd [zip::ParseDataHeader $zipfd sb] start + if { $sb(method) != 0} { + set nfd [::zip::zstream $zipfd $sb(csize) $sb(size)] + } else { + set nfd [::zip::rawstream $zipfd $sb(size)] + } + return [list $nfd] + } else { + set nfd [vfs::memchan] + fconfigure $nfd -translation binary + + set data [zip::Data $zipfd sb 0] + + puts -nonewline $nfd $data + + fconfigure $nfd -translation auto + seek $nfd 0 + return [list $nfd] + } + } + default { + vfs::filesystem posixerror $::vfs::posix(EROFS) + } + } +} + +proc vfs::zip::createdirectory {zipfd name} { + #::vfs::log "createdirectory $name" + vfs::filesystem posixerror $::vfs::posix(EROFS) +} + +proc vfs::zip::removedirectory {zipfd name recursive} { + #::vfs::log "removedirectory $name" + vfs::filesystem posixerror $::vfs::posix(EROFS) +} + +proc vfs::zip::deletefile {zipfd name} { + #::vfs::log "deletefile $name" + vfs::filesystem posixerror $::vfs::posix(EROFS) +} + +proc vfs::zip::fileattributes {zipfd name args} { + #::vfs::log "fileattributes $args" + switch -- [llength $args] { + 0 { + # list strings + return [list] + } + 1 { + # get value + set index [lindex $args 0] + return "" + } + 2 { + # set value + set index [lindex $args 0] + set val [lindex $args 1] + vfs::filesystem posixerror $::vfs::posix(EROFS) + } + } +} + +proc vfs::zip::utime {fd path actime mtime} { + vfs::filesystem posixerror $::vfs::posix(EROFS) +} + +# Below copied from TclKit distribution + +# +# ZIP decoder: +# +# See the ZIP file format specification: +# http://www.pkware.com/documents/casestudies/APPNOTE.TXT +# +# Format of zip file: +# [ Data ]* [ TOC ]* EndOfArchive +# +# Note: TOC is refered to in ZIP doc as "Central Archive" +# +# This means there are two ways of accessing: +# +# 1) from the begining as a stream - until the header +# is not "PK\03\04" - ideal for unzipping. +# +# 2) for table of contents without reading entire +# archive by first fetching EndOfArchive, then +# just loading the TOC +# + +namespace eval zip { + set zseq 0 + + array set methods { + 0 {stored - The file is stored (no compression)} + 1 {shrunk - The file is Shrunk} + 2 {reduce1 - The file is Reduced with compression factor 1} + 3 {reduce2 - The file is Reduced with compression factor 2} + 4 {reduce3 - The file is Reduced with compression factor 3} + 5 {reduce4 - The file is Reduced with compression factor 4} + 6 {implode - The file is Imploded} + 7 {reserved - Reserved for Tokenizing compression algorithm} + 8 {deflate - The file is Deflated} + 9 {reserved - Reserved for enhanced Deflating} + 10 {pkimplode - PKWARE Date Compression Library Imploding} + 11 {reserved - Reserved by PKWARE} + 12 {bzip2 - The file is compressed using BZIP2 algorithm} + 13 {reserved - Reserved by PKWARE} + 14 {lzma - LZMA (EFS)} + 15 {reserved - Reserved by PKWARE} + } + # Version types (high-order byte) + array set systems { + 0 {dos} + 1 {amiga} + 2 {vms} + 3 {unix} + 4 {vm cms} + 5 {atari} + 6 {os/2} + 7 {macos} + 8 {z system 8} + 9 {cp/m} + 10 {tops20} + 11 {windows} + 12 {qdos} + 13 {riscos} + 14 {vfat} + 15 {mvs} + 16 {beos} + 17 {tandem} + 18 {theos} + } + # DOS File Attrs + array set dosattrs { + 1 {readonly} + 2 {hidden} + 4 {system} + 8 {unknown8} + 16 {directory} + 32 {archive} + 64 {unknown64} + 128 {normal} + } + + proc u_short {n} { return [expr { ($n+0x10000)%0x10000 }] } +} + +proc zip::DosTime {date time} { + set time [u_short $time] + set date [u_short $date] + + # time = fedcba9876543210 + # HHHHHmmmmmmSSSSS (sec/2 actually) + + # data = fedcba9876543210 + # yyyyyyyMMMMddddd + + set sec [expr { ($time & 0x1F) * 2 }] + set min [expr { ($time >> 5) & 0x3F }] + set hour [expr { ($time >> 11) & 0x1F }] + + set mday [expr { $date & 0x1F }] + set mon [expr { (($date >> 5) & 0xF) }] + set year [expr { (($date >> 9) & 0xFF) + 1980 }] + + # Fix up bad date/time data, no need to fail + if {$sec > 59} {set sec 59} + if {$min > 59} {set min 59} + if {$hour > 23} {set hour 23} + if {$mday < 1} {set mday 1} + if {$mday > 31} {set mday 31} + if {$mon < 1} {set mon 1} + if {$mon > 12} {set mon 12} + + set res 0 + catch { + set dt [format {%4.4d-%2.2d-%2.2d %2.2d:%2.2d:%2.2d} \ + $year $mon $mday $hour $min $sec] + set res [clock scan $dt -gmt 1] + } + + return $res +} + +proc zip::ParseDataHeader {fd arr {dataVar ""}} { + upvar 1 $arr sb + + upvar 1 $arr sb + + # APPNOTE A: Local file header + set buf [read $fd 30] + set n [binary scan $buf A4sssssiiiss \ + hdr sb(ver) sb(flags) sb(method) time date \ + crc csize size namelen xtralen] + + if { ![string equal "PK\03\04" $hdr] } { + binary scan $hdr H* x + return -code error "bad header: $x" + } + set sb(ver) [expr {$sb(ver) & 0xffff}] + set sb(flags) [expr {$sb(flags) & 0xffff}] + set sb(method) [expr {$sb(method) & 0xffff}] + set sb(mtime) [DosTime $date $time] + if {!($sb(flags) & (1<<3))} { + set sb(crc) [expr {$crc & 0xffffffff}] + set sb(csize) [expr {$csize & 0xffffffff}] + set sb(size) [expr {$size & 0xffffffff}] + } + + set sb(name) [read $fd [expr {$namelen & 0xffff}]] + set sb(extra) [read $fd [expr {$xtralen & 0xffff}]] + if {$sb(flags) & (1 << 11)} { + set sb(name) [encoding convertfrom utf-8 $sb(name)] + } + set sb(name) [string trimleft $sb(name) "./"] + + # APPNOTE B: File data + # if bit 3 of flags is set the csize comes from the central directory + set offset [tell $fd] + if {$dataVar != ""} { + upvar 1 $dataVar data + set data [read $fd $sb(csize)] + } else { + seek $fd $sb(csize) current + } + + # APPNOTE C: Data descriptor + if { $sb(flags) & (1<<3) } { + binary scan [read $fd 4] i ddhdr + if {($ddhdr & 0xffffffff) == 0x08074b50} { + binary scan [read $fd 12] iii sb(crc) sb(csize) sb(size) + } else { + set sb(crc) $ddhdr + binary scan [read $fd 8] ii sb(csize) sb(size) + } + set sb(crc) [expr {$sb(crc) & 0xffffffff}] + set sb(csize) [expr {$sb(csize) & 0xffffffff}] + set sb(size) [expr {$sb(size) & 0xffffffff}] + } + return $offset +} + +proc zip::Data {fd arr verify} { + upvar 1 $arr sb + ParseDataHeader $fd $arr data + switch -exact -- $sb(method) { + 0 { + # stored; no compression + } + 8 { + # deflated + if {[catch { + set data [vfs::zip -mode decompress -nowrap 1 $data] + } err]} then { + return -code error "error inflating \"$sb(name)\": $err" + } + } + default { + set method $sb(method) + if {[info exists methods($method)]} { + set method $methods($method) + } + return -code error "unsupported compression method + \"$method\" used for \"$sb(name)\"" + } + } + + if { $verify && $sb(method) != 0} { + set ncrc [vfs::crc $data] + if { ($ncrc & 0xffffffff) != $sb(crc) } { + vfs::log [format {%s: crc mismatch: expected 0x%x, got 0x%x} \ + $sb(name) $sb(crc) $ncrc] + } + } + return $data +} + +proc zip::EndOfArchive {fd arr} { + upvar 1 $arr cb + + # [SF Tclvfs Bug 1003574]. Do not seek over beginning of file. + seek $fd 0 end + + # Just looking in the last 512 bytes may be enough to handle zip + # archives without comments, however for archives which have + # comments the chunk may start at an arbitrary distance from the + # end of the file. So if we do not find the header immediately + # we have to extend the range of our search, possibly until we + # have a large part of the archive in memory. We can fail only + # after the whole file has been searched. + + set sz [tell $fd] + if {[info exists ::zip::max_header_seek]} { + if {$::zip::max_header_seek < $sz} { + set sz $::zip::max_header_seek + } + } + set len 512 + set at 512 + while {1} { + if {$sz < $at} {set n -$sz} else {set n -$at} + + seek $fd $n end + set hdr [read $fd $len] + + # We are using 'string last' as we are searching the first + # from the end, which is the last from the beginning. See [SF + # Bug 2256740]. A zip archive stored in a zip archive can + # confuse the unmodified code, triggering on the magic + # sequence for the inner, uncompressed archive. + set pos [string last "PK\05\06" $hdr] + if {$pos < 0} { + if {$at >= $sz} { + return -code error "no header found" + } + set len 540 ; # after 1st iteration we force overlap with last buffer + incr at 512 ; # to ensure that the pattern we look for is not split at + # ; # a buffer boundary, nor the header itself + } else { + break + } + } + + set hdrlen [string length $hdr] + set hdr [string range $hdr [expr $pos + 4] [expr $pos + 21]] + + set pos [expr {wide([tell $fd]) + $pos - $hdrlen}] + + if {$pos < 0} { + set pos 0 + } + + binary scan $hdr ssssiis \ + cb(ndisk) cb(cdisk) \ + cb(nitems) cb(ntotal) \ + cb(csize) cb(coff) \ + cb(comment) + + set cb(ndisk) [u_short $cb(ndisk)] + set cb(nitems) [u_short $cb(nitems)] + set cb(ntotal) [u_short $cb(ntotal)] + set cb(comment) [u_short $cb(comment)] + + # Compute base for situations where ZIP file + # has been appended to another media (e.g. EXE) + set base [expr { $pos - $cb(csize) - $cb(coff) }] + if {$base < 0} { + set base 0 + } + set cb(base) $base + + if {$cb(coff) < 0} { + set cb(base) [expr {wide($cb(base)) - 4294967296}] + set cb(coff) [expr {wide($cb(coff)) + 4294967296}] + } +} + +proc zip::TOC {fd arr} { + upvar #0 zip::$fd cb + upvar #0 zip::$fd.dir cbdir + upvar 1 $arr sb + + set buf [read $fd 46] + + binary scan $buf A4ssssssiiisssssii hdr \ + sb(vem) sb(ver) sb(flags) sb(method) time date \ + sb(crc) sb(csize) sb(size) \ + flen elen clen sb(disk) sb(attr) \ + sb(atx) sb(ino) + + set sb(ino) [expr {$cb(base) + $sb(ino)}] + + if { ![string equal "PK\01\02" $hdr] } { + binary scan $hdr H* x + return -code error "bad central header: $x" + } + + foreach v {vem ver flags method disk attr} { + set sb($v) [expr {$sb($v) & 0xffff}] + } + set sb(crc) [expr {$sb(crc) & 0xffffffff}] + set sb(csize) [expr {$sb(csize) & 0xffffffff}] + set sb(size) [expr {$sb(size) & 0xffffffff}] + set sb(mtime) [DosTime $date $time] + set sb(mode) [expr { ($sb(atx) >> 16) & 0xffff }] + # check atx field or mode field if this is a directory + if { ((( $sb(atx) & 0xff ) & 16) != 0) || (($sb(mode) & 0x4000) != 0) } { + set sb(type) directory + } else { + set sb(type) file + } + set sb(name) [read $fd [u_short $flen]] + set sb(extra) [read $fd [u_short $elen]] + set sb(comment) [read $fd [u_short $clen]] + while {$sb(ino) < 0} { + set sb(ino) [expr {wide($sb(ino)) + 4294967296}] + } + if {$sb(flags) & (1 << 11)} { + set sb(name) [encoding convertfrom utf-8 $sb(name)] + set sb(comment) [encoding convertfrom utf-8 $sb(comment)] + } + set sb(name) [string trimleft $sb(name) "./"] + set parent [file dirname $sb(name)] + if {$parent == "."} {set parent ""} + lappend cbdir([string tolower $parent]) [file tail [string trimright $sb(name) /]] +} + +proc zip::open {path} { + #vfs::log [list open $path] + set fd [::open $path] + + if {[catch { + upvar #0 zip::$fd cb + upvar #0 zip::$fd.toc toc + upvar #0 zip::$fd.dir cbdir + + fconfigure $fd -translation binary ;#-buffering none + + zip::EndOfArchive $fd cb + + seek $fd [expr {$cb(base) + $cb(coff)}] start + + set toc(_) 0; unset toc(_); #MakeArray + + for {set i 0} {$i < $cb(nitems)} {incr i} { + zip::TOC $fd sb + + set origname [string trimright $sb(name) /] + set sb(depth) [llength [file split $sb(name)]] + + set name [string tolower $origname] + set sba [array get sb] + set toc($name) $sba + FAKEDIR toc cbdir [file dirname $origname] + } + foreach {n v} [array get cbdir] { + set cbdir($n) [lsort -unique $v] + } + } err]} { + close $fd + return -code error $err + } + + return $fd +} + +proc zip::FAKEDIR {tocarr cbdirarr origpath} { + upvar 1 $tocarr toc $cbdirarr cbdir + + set path [string tolower $origpath] + if { $path == "."} { return } + + if { ![info exists toc($path)] } { + # Implicit directory + lappend toc($path) \ + name $origpath \ + type directory mtime 0 size 0 mode 0777 \ + ino -1 depth [llength [file split $path]] + + set parent [file dirname $path] + if {$parent == "."} {set parent ""} + lappend cbdir($parent) [file tail $origpath] + } + FAKEDIR toc cbdir [file dirname $origpath] +} + +proc zip::exists {fd path} { + #::vfs::log "$fd $path" + if {$path == ""} { + return 1 + } else { + upvar #0 zip::$fd.toc toc + info exists toc([string tolower $path]) + } +} + +proc zip::stat {fd path arr} { + upvar #0 zip::$fd.toc toc + upvar 1 $arr sb + #vfs::log [list stat $fd $path $arr [info level -1]] + + set name [string tolower $path] + if { $name == "" || $name == "." } { + array set sb { + type directory mtime 0 size 0 mode 0777 + ino -1 depth 0 name "" + } + } elseif {![info exists toc($name)] } { + return -code error "could not read \"$path\": no such file or directory" + } else { + array set sb $toc($name) + } + set sb(dev) -1 + set sb(uid) -1 + set sb(gid) -1 + set sb(nlink) 1 + set sb(atime) $sb(mtime) + set sb(ctime) $sb(mtime) + return "" +} + +# Treats empty pattern as asking for a particular file only +proc zip::getdir {fd path {pat *}} { + #::vfs::log [list getdir $fd $path $pat] + upvar #0 zip::$fd.toc toc + upvar #0 zip::$fd.dir cbdir + + if { $path == "." || $path == "" } { + set path "" + } else { + set path [string tolower $path] + } + + if {$pat == ""} { + if {[info exists cbdir($path)]} { + return [list $path] + } else { + return [list] + } + } + + set rc [list] + if {[info exists cbdir($path)]} { + if {$pat == "*"} { + set rc $cbdir($path) + } else { + foreach f $cbdir($path) { + if {[string match -nocase $pat $f]} { + lappend rc $f + } + } + } + } + return $rc +} + +proc zip::_close {fd} { + variable $fd + variable $fd.toc + variable $fd.dir + unset $fd + unset $fd.toc + unset $fd.dir + ::close $fd +} + +# Implementation of stream based decompression for zip +if {([info commands ::rechan] != "") || ([info commands ::chan] != "")} { + if {![catch {package require Tcl 8.6}]} { + # implementation using [zlib stream inflate] and [rechan]/[chan create] + proc ::zip::zstream_create {fd} { + upvar #0 ::zip::_zstream_zcmd($fd) zcmd + if {$zcmd == ""} { + set zcmd [zlib stream inflate] + } + } + proc ::zip::zstream_delete {fd} { + upvar #0 ::zip::_zstream_zcmd($fd) zcmd + if {$zcmd != ""} { + rename $zcmd "" + set zcmd "" + } + } + + proc ::zip::zstream_put {fd data} { + upvar #0 ::zip::_zstream_zcmd($fd) zcmd + zstream_create $fd + $zcmd put $data + } + + proc ::zip::zstream_get {fd} { + upvar #0 ::zip::_zstream_zcmd($fd) zcmd + zstream_create $fd + return [$zcmd get] + } + + set ::zip::useStreaming 1 + } elseif {![catch {zlib sinflate ::zip::__dummycommand ; rename ::zip::__dummycommand ""}]} { + proc ::zip::zstream_create {fd} { + upvar #0 ::zip::_zstream_zcmd($fd) zcmd + if {$zcmd == ""} { + set zcmd ::zip::_zstream_cmd_$fd + zlib sinflate $zcmd + } + } + proc ::zip::zstream_delete {fd} { + upvar #0 ::zip::_zstream_zcmd($fd) zcmd + if {$zcmd != ""} { + rename $zcmd "" + set zcmd "" + } + } + + proc ::zip::zstream_put {fd data} { + upvar #0 ::zip::_zstream_zcmd($fd) zcmd + zstream_create $fd + $zcmd fill $data + } + + proc ::zip::zstream_get {fd} { + upvar #0 ::zip::_zstream_zcmd($fd) zcmd + zstream_create $fd + set rc "" + while {[$zcmd fill] != 0} { + if {[catch { + append rc [$zcmd drain 4096] + }]} { + break + } + } + return $rc + } + + set ::zip::useStreaming 1 + } else { + set ::zip::useStreaming 0 + } +} else { + set ::zip::useStreaming 0 +} + +proc ::zip::eventClean {fd} { + variable eventEnable + eventSet $fd 0 +} + +proc ::zip::eventWatch {fd a} { + if {[lindex $a 0] == "read"} { + eventSet $fd 1 + } else { + eventSet $fd 0 + } +} + +proc zip::eventSet {fd e} { + variable eventEnable + set cmd [list ::zip:::eventPost $fd] + after cancel $cmd + if {$e} { + set eventEnable($fd) 1 + after 0 $cmd + } else { + catch {unset eventEnable($fd)} + } +} + +proc zip::eventPost {fd} { + variable eventEnable + if {[info exists eventEnable($fd)] && $eventEnable($fd)} { + chan postevent $fd read + eventSet $fd 1 + } +} + +proc ::zip::zstream {ifd clen ilen} { + set start [tell $ifd] + set cmd [list ::zip::zstream_handler $start $ifd $clen $ilen] + if {[catch { + set fd [chan create read $cmd] + }]} { + set fd [rechan $cmd 2] + } + set ::zip::_zstream_buf($fd) "" + set ::zip::_zstream_pos($fd) 0 + set ::zip::_zstream_tell($fd) $start + set ::zip::_zstream_zcmd($fd) "" + return $fd +} + +proc ::zip::zstream_handler {istart ifd clen ilen cmd fd {a1 ""} {a2 ""}} { + upvar #0 ::zip::_zstream_pos($fd) pos + upvar #0 ::zip::_zstream_buf($fd) buf + upvar #0 ::zip::_zstream_tell($fd) tell + upvar #0 ::zip::_zstream_zcmd($fd) zcmd + switch -- $cmd { + initialize { + return [list initialize finalize watch read seek] + } + watch { + eventWatch $fd $a1 + } + seek { + switch $a2 { + 1 - current { incr a1 $pos } + 2 - end { incr a1 $ilen } + } + # to seek back, rewind, i.e. start from scratch + if {$a1 < $pos} { + zstream_delete $fd + seek $ifd $istart + set pos 0 + set buf "" + set tell $istart + } + + while {$pos < $a1} { + set n [expr {$a1 - $pos}] + if {$n > 4096} { set n 4096 } + zstream_handler $istart $ifd $clen $ilen read $fd $n + } + return $pos + } + + read { + set r "" + set n $a1 + if {$n + $pos > $ilen} { set n [expr {$ilen - $pos}] } + + while {$n > 0} { + set chunk [string range $buf 0 [expr {$n - 1}]] + set buf [string range $buf $n end] + incr n -[string length $chunk] + incr pos [string length $chunk] + append r $chunk + + if {$n > 0} { + set c [expr {$istart + $clen - [tell $ifd]}] + if {$c > 4096} { set c 4096 } + if {$c <= 0} { + break + } + seek $ifd $tell start + set data [read $ifd $c] + set tell [tell $ifd] + zstream_put $fd $data + while {[string length [set bufdata [zstream_get $fd]]] > 0} { + append buf $bufdata + } + } + } + return $r + } + close - finalize { + eventClean $fd + if {$zcmd != ""} { + rename $zcmd "" + } + unset pos + } + } +} + +proc ::zip::rawstream_handler {ifd ioffset ilen cmd fd {a1 ""} {a2 ""} args} { + upvar ::zip::_rawstream_pos($fd) pos + switch -- $cmd { + initialize { + return [list initialize finalize watch read seek] + } + watch { + eventWatch $fd $a1 + } + seek { + switch $a2 { + 1 - current { incr a1 $pos } + 2 - end { incr a1 $ilen } + } + if {$a1 < 0} {set a1 0} + if {$a1 > $ilen} {set a1 $ilen} + set pos $a1 + return $pos + } + read { + seek $ifd $ioffset + seek $ifd $pos current + set n $a1 + if {$n + $pos > $ilen} { set n [expr {$ilen - $pos}] } + set fc [read $ifd $n] + incr pos [string length $fc] + return $fc + } + close - finalize { + eventClean $fd + unset pos + } + } +} + +proc ::zip::rawstream {ifd ilen} { + set cname _rawstream_[incr ::zip::zseq] + set start [tell $ifd] + set cmd [list ::zip::rawstream_handler $ifd $start $ilen] + if {[catch { + set fd [chan create read $cmd] + }]} { + set fd [rechan $cmd 2] + } + set ::zip::_rawstream_pos($fd) 0 + return $fd +} + diff --git a/src/lib/vfszip/pkgIndex.tcl b/src/lib/vfszip/pkgIndex.tcl new file mode 100644 index 00000000..60421d79 --- /dev/null +++ b/src/lib/vfszip/pkgIndex.tcl @@ -0,0 +1,53 @@ +# -*- tcl -*- +# Tcl package index file, version 1.1 +# This file was generated by hand. +# +# This will be autogenerated by configure to use the correct name +# for the vfs dynamic library. + +#package ifneeded vfs 1.5.0 [list source [file join $dir vfs.tcl]] +# +#package ifneeded starkit 1.3.3 [list source [file join $dir starkit.tcl]] +# +## New, for the old, keep version numbers synchronized. +#package ifneeded vfs::mk4 1.10.1 [list source [file join $dir mk4vfs.tcl]] + + +#2025 - provide a fix for 'bad central header' error in zip::open when platform has older vfs library +package ifneeded vfs::zip 1.0.4 [list source [file join $dir zipvfs.tcl]] + +# New +#package ifneeded vfs::ftp 1.0 [list source [file join $dir ftpvfs.tcl]] +#package ifneeded vfs::http 0.6 [list source [file join $dir httpvfs.tcl]] +#package ifneeded vfs::ns 0.5.1 [list source [file join $dir tclprocvfs.tcl]] +#package ifneeded vfs::tar 0.91 [list source [file join $dir tarvfs.tcl]] +#package ifneeded vfs::test 1.0 [list source [file join $dir testvfs.tcl]] +#package ifneeded vfs::urltype 1.0 [list source [file join $dir vfsUrl.tcl]] +#package ifneeded vfs::webdav 0.1 [list source [file join $dir webdavvfs.tcl]] +#package ifneeded vfs::tk 0.5 [list source [file join $dir tkvfs.tcl]] +## +## Virtual filesystems based on the template vfs: +## +#if {[lsearch -exact $::auto_path [file join $dir template]] < 0} { +# lappend ::auto_path [file join $dir template] +#} +#package ifneeded vfs::template::chroot 1.5.2 \ +# [list source [file join $dir template chrootvfs.tcl]] +#package ifneeded vfs::template::collate 1.5.3 \ +# [list source [file join $dir template collatevfs.tcl]] +#package ifneeded vfs::template::version 1.5.2 \ +# [list source [file join $dir template versionvfs.tcl]] +#package ifneeded vfs::template::version::delta 1.5.2 \ +# [list source [file join $dir template deltavfs.tcl]] +#package ifneeded vfs::template::fish 1.5.2 \ +# [list source [file join $dir template fishvfs.tcl]] +#package ifneeded vfs::template::quota 1.5.2 \ +# [list source [file join $dir template quotavfs.tcl]] +#package ifneeded vfs::template 1.5.5 \ +# [list source [file join $dir template templatevfs.tcl]] +## +## Helpers +## +#package ifneeded fileutil::globfind 1.5 \ +# [list source [file join $dir template globfind.tcl]] +#package ifneeded trsync 1.0 [list source [file join $dir template tdelta.tcl]] diff --git a/src/lib/vfszip/zipvfs.tcl b/src/lib/vfszip/zipvfs.tcl new file mode 100644 index 00000000..0a0ef767 --- /dev/null +++ b/src/lib/vfszip/zipvfs.tcl @@ -0,0 +1,937 @@ +# Removed provision of the backward compatible name. Moved to separate +# file/package. +package provide vfs::zip 1.0.4 + +package require vfs + +# Using the vfs, memchan and Trf extensions, we ought to be able +# to write a Tcl-only zip virtual filesystem. What we have below +# is basically that. + +namespace eval vfs::zip {} + +# Used to execute a zip archive. This is rather like a jar file +# but simpler. We simply mount it and then source a toplevel +# file called 'main.tcl'. +proc vfs::zip::Execute {zipfile} { + Mount $zipfile $zipfile + source [file join $zipfile main.tcl] +} + +proc vfs::zip::Mount {zipfile local} { + set fd [::zip::open [::file normalize $zipfile]] + vfs::filesystem mount $local [list ::vfs::zip::handler $fd] + # Register command to unmount + vfs::RegisterMount $local [list ::vfs::zip::Unmount $fd] + return $fd +} + +proc vfs::zip::Unmount {fd local} { + vfs::filesystem unmount $local + ::zip::_close $fd +} + +proc vfs::zip::handler {zipfd cmd root relative actualpath args} { + #::vfs::log [list $zipfd $cmd $root $relative $actualpath $args] + if {$cmd == "matchindirectory"} { + eval [list $cmd $zipfd $relative $actualpath] $args + } else { + eval [list $cmd $zipfd $relative] $args + } +} + +proc vfs::zip::attributes {zipfd} { return [list "state"] } +proc vfs::zip::state {zipfd args} { + vfs::attributeCantConfigure "state" "readonly" $args +} + +# If we implement the commands below, we will have a perfect +# virtual file system for zip files. + +proc vfs::zip::matchindirectory {zipfd path actualpath pattern type} { + #::vfs::log [list matchindirectory $path $actualpath $pattern $type] + + # This call to zip::getdir handles empty patterns properly as asking + # for the existence of a single file $path only + set res [::zip::getdir $zipfd $path $pattern] + #::vfs::log "got $res" + if {![string length $pattern]} { + if {![::zip::exists $zipfd $path]} { return {} } + set res [list $actualpath] + set actualpath "" + } + + set newres [list] + foreach p [::vfs::matchCorrectTypes $type $res $actualpath] { + lappend newres [file join $actualpath $p] + } + #::vfs::log "got $newres" + return $newres +} + +proc vfs::zip::stat {zipfd name} { + #::vfs::log "stat $name" + ::zip::stat $zipfd $name sb + #::vfs::log [array get sb] + # remove socket mode file type (0xc000) to prevent Tcl from reporting Fossil archives as socket types + if {($sb(mode) & 0xf000) == 0xc000} { + set sb(mode) [expr {$sb(mode) ^ 0xc000}] + } + # remove block device bit file type (0x6000) + if {($sb(mode) & 0xf000) == 0x6000} { + set sb(mode) [expr {$sb(mode) ^ 0x6000}] + } + # remove FIFO mode file type (0x1000) + if {($sb(mode) & 0xf000) == 0x1000} { + set sb(mode) [expr {$sb(mode) ^ 0x1000}] + } + # remove character device mode file type (0x2000) + if {($sb(mode) & 0xf000) == 0x2000} { + set sb(mode) [expr {$sb(mode) ^ 0x2000}] + } + # workaround for certain errorneus zip archives + if {($sb(mode) & 0xffff) == 0xffff} { + # change to directory type and set mode to 0777 + directory flag + set sb(mode) 0x41ff + } + array get sb +} + +proc vfs::zip::access {zipfd name mode} { + #::vfs::log "zip-access $name $mode" + if {$mode & 2} { + vfs::filesystem posixerror $::vfs::posix(EROFS) + } + # Readable, Exists and Executable are treated as 'exists' + # Could we get more information from the archive? + if {[::zip::exists $zipfd $name]} { + return 1 + } else { + error "No such file" + } + +} + +proc vfs::zip::open {zipfd name mode permissions} { + #::vfs::log "open $name $mode $permissions" + # return a list of two elements: + # 1. first element is the Tcl channel name which has been opened + # 2. second element (optional) is a command to evaluate when + # the channel is closed. + + switch -- $mode { + "" - + "r" { + if {![::zip::exists $zipfd $name]} { + vfs::filesystem posixerror $::vfs::posix(ENOENT) + } + + ::zip::stat $zipfd $name sb + + if {$sb(ino) < 0} { + vfs::filesystem posixerror $::vfs::posix(EISDIR) + } + +# set nfd [vfs::memchan] +# fconfigure $nfd -translation binary + + seek $zipfd $sb(ino) start +# set data [zip::Data $zipfd sb 0] + +# puts -nonewline $nfd $data + +# fconfigure $nfd -translation auto +# seek $nfd 0 +# return [list $nfd] + # use streaming for files larger than 1MB + if {$::zip::useStreaming && $sb(size) >= 1048576} { + seek $zipfd [zip::ParseDataHeader $zipfd sb] start + if { $sb(method) != 0} { + set nfd [::zip::zstream $zipfd $sb(csize) $sb(size)] + } else { + set nfd [::zip::rawstream $zipfd $sb(size)] + } + return [list $nfd] + } else { + set nfd [vfs::memchan] + fconfigure $nfd -translation binary + + set data [zip::Data $zipfd sb 0] + + puts -nonewline $nfd $data + + fconfigure $nfd -translation auto + seek $nfd 0 + return [list $nfd] + } + } + default { + vfs::filesystem posixerror $::vfs::posix(EROFS) + } + } +} + +proc vfs::zip::createdirectory {zipfd name} { + #::vfs::log "createdirectory $name" + vfs::filesystem posixerror $::vfs::posix(EROFS) +} + +proc vfs::zip::removedirectory {zipfd name recursive} { + #::vfs::log "removedirectory $name" + vfs::filesystem posixerror $::vfs::posix(EROFS) +} + +proc vfs::zip::deletefile {zipfd name} { + #::vfs::log "deletefile $name" + vfs::filesystem posixerror $::vfs::posix(EROFS) +} + +proc vfs::zip::fileattributes {zipfd name args} { + #::vfs::log "fileattributes $args" + switch -- [llength $args] { + 0 { + # list strings + return [list] + } + 1 { + # get value + set index [lindex $args 0] + return "" + } + 2 { + # set value + set index [lindex $args 0] + set val [lindex $args 1] + vfs::filesystem posixerror $::vfs::posix(EROFS) + } + } +} + +proc vfs::zip::utime {fd path actime mtime} { + vfs::filesystem posixerror $::vfs::posix(EROFS) +} + +# Below copied from TclKit distribution + +# +# ZIP decoder: +# +# See the ZIP file format specification: +# http://www.pkware.com/documents/casestudies/APPNOTE.TXT +# +# Format of zip file: +# [ Data ]* [ TOC ]* EndOfArchive +# +# Note: TOC is refered to in ZIP doc as "Central Archive" +# +# This means there are two ways of accessing: +# +# 1) from the begining as a stream - until the header +# is not "PK\03\04" - ideal for unzipping. +# +# 2) for table of contents without reading entire +# archive by first fetching EndOfArchive, then +# just loading the TOC +# + +namespace eval zip { + set zseq 0 + + array set methods { + 0 {stored - The file is stored (no compression)} + 1 {shrunk - The file is Shrunk} + 2 {reduce1 - The file is Reduced with compression factor 1} + 3 {reduce2 - The file is Reduced with compression factor 2} + 4 {reduce3 - The file is Reduced with compression factor 3} + 5 {reduce4 - The file is Reduced with compression factor 4} + 6 {implode - The file is Imploded} + 7 {reserved - Reserved for Tokenizing compression algorithm} + 8 {deflate - The file is Deflated} + 9 {reserved - Reserved for enhanced Deflating} + 10 {pkimplode - PKWARE Date Compression Library Imploding} + 11 {reserved - Reserved by PKWARE} + 12 {bzip2 - The file is compressed using BZIP2 algorithm} + 13 {reserved - Reserved by PKWARE} + 14 {lzma - LZMA (EFS)} + 15 {reserved - Reserved by PKWARE} + } + # Version types (high-order byte) + array set systems { + 0 {dos} + 1 {amiga} + 2 {vms} + 3 {unix} + 4 {vm cms} + 5 {atari} + 6 {os/2} + 7 {macos} + 8 {z system 8} + 9 {cp/m} + 10 {tops20} + 11 {windows} + 12 {qdos} + 13 {riscos} + 14 {vfat} + 15 {mvs} + 16 {beos} + 17 {tandem} + 18 {theos} + } + # DOS File Attrs + array set dosattrs { + 1 {readonly} + 2 {hidden} + 4 {system} + 8 {unknown8} + 16 {directory} + 32 {archive} + 64 {unknown64} + 128 {normal} + } + + proc u_short {n} { return [expr { ($n+0x10000)%0x10000 }] } +} + +proc zip::DosTime {date time} { + set time [u_short $time] + set date [u_short $date] + + # time = fedcba9876543210 + # HHHHHmmmmmmSSSSS (sec/2 actually) + + # data = fedcba9876543210 + # yyyyyyyMMMMddddd + + set sec [expr { ($time & 0x1F) * 2 }] + set min [expr { ($time >> 5) & 0x3F }] + set hour [expr { ($time >> 11) & 0x1F }] + + set mday [expr { $date & 0x1F }] + set mon [expr { (($date >> 5) & 0xF) }] + set year [expr { (($date >> 9) & 0xFF) + 1980 }] + + # Fix up bad date/time data, no need to fail + if {$sec > 59} {set sec 59} + if {$min > 59} {set min 59} + if {$hour > 23} {set hour 23} + if {$mday < 1} {set mday 1} + if {$mday > 31} {set mday 31} + if {$mon < 1} {set mon 1} + if {$mon > 12} {set mon 12} + + set res 0 + catch { + set dt [format {%4.4d-%2.2d-%2.2d %2.2d:%2.2d:%2.2d} \ + $year $mon $mday $hour $min $sec] + set res [clock scan $dt -gmt 1] + } + + return $res +} + +proc zip::ParseDataHeader {fd arr {dataVar ""}} { + upvar 1 $arr sb + + upvar 1 $arr sb + + # APPNOTE A: Local file header + set buf [read $fd 30] + set n [binary scan $buf A4sssssiiiss \ + hdr sb(ver) sb(flags) sb(method) time date \ + crc csize size namelen xtralen] + + if { ![string equal "PK\03\04" $hdr] } { + binary scan $hdr H* x + return -code error "bad header: $x" + } + set sb(ver) [expr {$sb(ver) & 0xffff}] + set sb(flags) [expr {$sb(flags) & 0xffff}] + set sb(method) [expr {$sb(method) & 0xffff}] + set sb(mtime) [DosTime $date $time] + if {!($sb(flags) & (1<<3))} { + set sb(crc) [expr {$crc & 0xffffffff}] + set sb(csize) [expr {$csize & 0xffffffff}] + set sb(size) [expr {$size & 0xffffffff}] + } + + set sb(name) [read $fd [expr {$namelen & 0xffff}]] + set sb(extra) [read $fd [expr {$xtralen & 0xffff}]] + if {$sb(flags) & (1 << 11)} { + set sb(name) [encoding convertfrom utf-8 $sb(name)] + } + set sb(name) [string trimleft $sb(name) "./"] + + # APPNOTE B: File data + # if bit 3 of flags is set the csize comes from the central directory + set offset [tell $fd] + if {$dataVar != ""} { + upvar 1 $dataVar data + set data [read $fd $sb(csize)] + } else { + seek $fd $sb(csize) current + } + + # APPNOTE C: Data descriptor + if { $sb(flags) & (1<<3) } { + binary scan [read $fd 4] i ddhdr + if {($ddhdr & 0xffffffff) == 0x08074b50} { + binary scan [read $fd 12] iii sb(crc) sb(csize) sb(size) + } else { + set sb(crc) $ddhdr + binary scan [read $fd 8] ii sb(csize) sb(size) + } + set sb(crc) [expr {$sb(crc) & 0xffffffff}] + set sb(csize) [expr {$sb(csize) & 0xffffffff}] + set sb(size) [expr {$sb(size) & 0xffffffff}] + } + return $offset +} + +proc zip::Data {fd arr verify} { + upvar 1 $arr sb + ParseDataHeader $fd $arr data + switch -exact -- $sb(method) { + 0 { + # stored; no compression + } + 8 { + # deflated + if {[catch { + set data [vfs::zip -mode decompress -nowrap 1 $data] + } err]} then { + return -code error "error inflating \"$sb(name)\": $err" + } + } + default { + set method $sb(method) + if {[info exists methods($method)]} { + set method $methods($method) + } + return -code error "unsupported compression method + \"$method\" used for \"$sb(name)\"" + } + } + + if { $verify && $sb(method) != 0} { + set ncrc [vfs::crc $data] + if { ($ncrc & 0xffffffff) != $sb(crc) } { + vfs::log [format {%s: crc mismatch: expected 0x%x, got 0x%x} \ + $sb(name) $sb(crc) $ncrc] + } + } + return $data +} + +proc zip::EndOfArchive {fd arr} { + upvar 1 $arr cb + + # [SF Tclvfs Bug 1003574]. Do not seek over beginning of file. + seek $fd 0 end + + # Just looking in the last 512 bytes may be enough to handle zip + # archives without comments, however for archives which have + # comments the chunk may start at an arbitrary distance from the + # end of the file. So if we do not find the header immediately + # we have to extend the range of our search, possibly until we + # have a large part of the archive in memory. We can fail only + # after the whole file has been searched. + + set sz [tell $fd] + if {[info exists ::zip::max_header_seek]} { + if {$::zip::max_header_seek < $sz} { + set sz $::zip::max_header_seek + } + } + set len 512 + set at 512 + while {1} { + if {$sz < $at} {set n -$sz} else {set n -$at} + + seek $fd $n end + set hdr [read $fd $len] + + # We are using 'string last' as we are searching the first + # from the end, which is the last from the beginning. See [SF + # Bug 2256740]. A zip archive stored in a zip archive can + # confuse the unmodified code, triggering on the magic + # sequence for the inner, uncompressed archive. + set pos [string last "PK\05\06" $hdr] + if {$pos < 0} { + if {$at >= $sz} { + return -code error "no header found" + } + set len 540 ; # after 1st iteration we force overlap with last buffer + incr at 512 ; # to ensure that the pattern we look for is not split at + # ; # a buffer boundary, nor the header itself + } else { + break + } + } + + set hdrlen [string length $hdr] + set hdr [string range $hdr [expr $pos + 4] [expr $pos + 21]] + + set pos [expr {wide([tell $fd]) + $pos - $hdrlen}] + + if {$pos < 0} { + set pos 0 + } + + binary scan $hdr ssssiis \ + cb(ndisk) cb(cdisk) \ + cb(nitems) cb(ntotal) \ + cb(csize) cb(coff) \ + cb(comment) + + set cb(ndisk) [u_short $cb(ndisk)] + set cb(nitems) [u_short $cb(nitems)] + set cb(ntotal) [u_short $cb(ntotal)] + set cb(comment) [u_short $cb(comment)] + + # Compute base for situations where ZIP file + # has been appended to another media (e.g. EXE) + set base [expr { $pos - $cb(csize) - $cb(coff) }] + if {$base < 0} { + set base 0 + } + set cb(base) $base + + if {$cb(coff) < 0} { + set cb(base) [expr {wide($cb(base)) - 4294967296}] + set cb(coff) [expr {wide($cb(coff)) + 4294967296}] + } +} + +proc zip::TOC {fd arr} { + upvar #0 zip::$fd cb + upvar #0 zip::$fd.dir cbdir + upvar 1 $arr sb + + set buf [read $fd 46] + + binary scan $buf A4ssssssiiisssssii hdr \ + sb(vem) sb(ver) sb(flags) sb(method) time date \ + sb(crc) sb(csize) sb(size) \ + flen elen clen sb(disk) sb(attr) \ + sb(atx) sb(ino) + + set sb(ino) [expr {$cb(base) + $sb(ino)}] + + if { ![string equal "PK\01\02" $hdr] } { + binary scan $hdr H* x + return -code error "bad central header: $x" + } + + foreach v {vem ver flags method disk attr} { + set sb($v) [expr {$sb($v) & 0xffff}] + } + set sb(crc) [expr {$sb(crc) & 0xffffffff}] + set sb(csize) [expr {$sb(csize) & 0xffffffff}] + set sb(size) [expr {$sb(size) & 0xffffffff}] + set sb(mtime) [DosTime $date $time] + set sb(mode) [expr { ($sb(atx) >> 16) & 0xffff }] + # check atx field or mode field if this is a directory + if { ((( $sb(atx) & 0xff ) & 16) != 0) || (($sb(mode) & 0x4000) != 0) } { + set sb(type) directory + } else { + set sb(type) file + } + set sb(name) [read $fd [u_short $flen]] + set sb(extra) [read $fd [u_short $elen]] + set sb(comment) [read $fd [u_short $clen]] + while {$sb(ino) < 0} { + set sb(ino) [expr {wide($sb(ino)) + 4294967296}] + } + if {$sb(flags) & (1 << 11)} { + set sb(name) [encoding convertfrom utf-8 $sb(name)] + set sb(comment) [encoding convertfrom utf-8 $sb(comment)] + } + set sb(name) [string trimleft $sb(name) "./"] + set parent [file dirname $sb(name)] + if {$parent == "."} {set parent ""} + lappend cbdir([string tolower $parent]) [file tail [string trimright $sb(name) /]] +} + +proc zip::open {path} { + #vfs::log [list open $path] + set fd [::open $path] + + if {[catch { + upvar #0 zip::$fd cb + upvar #0 zip::$fd.toc toc + upvar #0 zip::$fd.dir cbdir + + fconfigure $fd -translation binary ;#-buffering none + + zip::EndOfArchive $fd cb + + seek $fd [expr {$cb(base) + $cb(coff)}] start + + set toc(_) 0; unset toc(_); #MakeArray + + for {set i 0} {$i < $cb(nitems)} {incr i} { + zip::TOC $fd sb + + set origname [string trimright $sb(name) /] + set sb(depth) [llength [file split $sb(name)]] + + set name [string tolower $origname] + set sba [array get sb] + set toc($name) $sba + FAKEDIR toc cbdir [file dirname $origname] + } + foreach {n v} [array get cbdir] { + set cbdir($n) [lsort -unique $v] + } + } err]} { + close $fd + return -code error $err + } + + return $fd +} + +proc zip::FAKEDIR {tocarr cbdirarr origpath} { + upvar 1 $tocarr toc $cbdirarr cbdir + + set path [string tolower $origpath] + if { $path == "."} { return } + + if { ![info exists toc($path)] } { + # Implicit directory + lappend toc($path) \ + name $origpath \ + type directory mtime 0 size 0 mode 0777 \ + ino -1 depth [llength [file split $path]] + + set parent [file dirname $path] + if {$parent == "."} {set parent ""} + lappend cbdir($parent) [file tail $origpath] + } + FAKEDIR toc cbdir [file dirname $origpath] +} + +proc zip::exists {fd path} { + #::vfs::log "$fd $path" + if {$path == ""} { + return 1 + } else { + upvar #0 zip::$fd.toc toc + info exists toc([string tolower $path]) + } +} + +proc zip::stat {fd path arr} { + upvar #0 zip::$fd.toc toc + upvar 1 $arr sb + #vfs::log [list stat $fd $path $arr [info level -1]] + + set name [string tolower $path] + if { $name == "" || $name == "." } { + array set sb { + type directory mtime 0 size 0 mode 0777 + ino -1 depth 0 name "" + } + } elseif {![info exists toc($name)] } { + return -code error "could not read \"$path\": no such file or directory" + } else { + array set sb $toc($name) + } + set sb(dev) -1 + set sb(uid) -1 + set sb(gid) -1 + set sb(nlink) 1 + set sb(atime) $sb(mtime) + set sb(ctime) $sb(mtime) + return "" +} + +# Treats empty pattern as asking for a particular file only +proc zip::getdir {fd path {pat *}} { + #::vfs::log [list getdir $fd $path $pat] + upvar #0 zip::$fd.toc toc + upvar #0 zip::$fd.dir cbdir + + if { $path == "." || $path == "" } { + set path "" + } else { + set path [string tolower $path] + } + + if {$pat == ""} { + if {[info exists cbdir($path)]} { + return [list $path] + } else { + return [list] + } + } + + set rc [list] + if {[info exists cbdir($path)]} { + if {$pat == "*"} { + set rc $cbdir($path) + } else { + foreach f $cbdir($path) { + if {[string match -nocase $pat $f]} { + lappend rc $f + } + } + } + } + return $rc +} + +proc zip::_close {fd} { + variable $fd + variable $fd.toc + variable $fd.dir + unset $fd + unset $fd.toc + unset $fd.dir + ::close $fd +} + +# Implementation of stream based decompression for zip +if {([info commands ::rechan] != "") || ([info commands ::chan] != "")} { + if {![catch {package require Tcl 8.6}]} { + # implementation using [zlib stream inflate] and [rechan]/[chan create] + proc ::zip::zstream_create {fd} { + upvar #0 ::zip::_zstream_zcmd($fd) zcmd + if {$zcmd == ""} { + set zcmd [zlib stream inflate] + } + } + proc ::zip::zstream_delete {fd} { + upvar #0 ::zip::_zstream_zcmd($fd) zcmd + if {$zcmd != ""} { + rename $zcmd "" + set zcmd "" + } + } + + proc ::zip::zstream_put {fd data} { + upvar #0 ::zip::_zstream_zcmd($fd) zcmd + zstream_create $fd + $zcmd put $data + } + + proc ::zip::zstream_get {fd} { + upvar #0 ::zip::_zstream_zcmd($fd) zcmd + zstream_create $fd + return [$zcmd get] + } + + set ::zip::useStreaming 1 + } elseif {![catch {zlib sinflate ::zip::__dummycommand ; rename ::zip::__dummycommand ""}]} { + proc ::zip::zstream_create {fd} { + upvar #0 ::zip::_zstream_zcmd($fd) zcmd + if {$zcmd == ""} { + set zcmd ::zip::_zstream_cmd_$fd + zlib sinflate $zcmd + } + } + proc ::zip::zstream_delete {fd} { + upvar #0 ::zip::_zstream_zcmd($fd) zcmd + if {$zcmd != ""} { + rename $zcmd "" + set zcmd "" + } + } + + proc ::zip::zstream_put {fd data} { + upvar #0 ::zip::_zstream_zcmd($fd) zcmd + zstream_create $fd + $zcmd fill $data + } + + proc ::zip::zstream_get {fd} { + upvar #0 ::zip::_zstream_zcmd($fd) zcmd + zstream_create $fd + set rc "" + while {[$zcmd fill] != 0} { + if {[catch { + append rc [$zcmd drain 4096] + }]} { + break + } + } + return $rc + } + + set ::zip::useStreaming 1 + } else { + set ::zip::useStreaming 0 + } +} else { + set ::zip::useStreaming 0 +} + +proc ::zip::eventClean {fd} { + variable eventEnable + eventSet $fd 0 +} + +proc ::zip::eventWatch {fd a} { + if {[lindex $a 0] == "read"} { + eventSet $fd 1 + } else { + eventSet $fd 0 + } +} + +proc zip::eventSet {fd e} { + variable eventEnable + set cmd [list ::zip:::eventPost $fd] + after cancel $cmd + if {$e} { + set eventEnable($fd) 1 + after 0 $cmd + } else { + catch {unset eventEnable($fd)} + } +} + +proc zip::eventPost {fd} { + variable eventEnable + if {[info exists eventEnable($fd)] && $eventEnable($fd)} { + chan postevent $fd read + eventSet $fd 1 + } +} + +proc ::zip::zstream {ifd clen ilen} { + set start [tell $ifd] + set cmd [list ::zip::zstream_handler $start $ifd $clen $ilen] + if {[catch { + set fd [chan create read $cmd] + }]} { + set fd [rechan $cmd 2] + } + set ::zip::_zstream_buf($fd) "" + set ::zip::_zstream_pos($fd) 0 + set ::zip::_zstream_tell($fd) $start + set ::zip::_zstream_zcmd($fd) "" + return $fd +} + +proc ::zip::zstream_handler {istart ifd clen ilen cmd fd {a1 ""} {a2 ""}} { + upvar #0 ::zip::_zstream_pos($fd) pos + upvar #0 ::zip::_zstream_buf($fd) buf + upvar #0 ::zip::_zstream_tell($fd) tell + upvar #0 ::zip::_zstream_zcmd($fd) zcmd + switch -- $cmd { + initialize { + return [list initialize finalize watch read seek] + } + watch { + eventWatch $fd $a1 + } + seek { + switch $a2 { + 1 - current { incr a1 $pos } + 2 - end { incr a1 $ilen } + } + # to seek back, rewind, i.e. start from scratch + if {$a1 < $pos} { + zstream_delete $fd + seek $ifd $istart + set pos 0 + set buf "" + set tell $istart + } + + while {$pos < $a1} { + set n [expr {$a1 - $pos}] + if {$n > 4096} { set n 4096 } + zstream_handler $istart $ifd $clen $ilen read $fd $n + } + return $pos + } + + read { + set r "" + set n $a1 + if {$n + $pos > $ilen} { set n [expr {$ilen - $pos}] } + + while {$n > 0} { + set chunk [string range $buf 0 [expr {$n - 1}]] + set buf [string range $buf $n end] + incr n -[string length $chunk] + incr pos [string length $chunk] + append r $chunk + + if {$n > 0} { + set c [expr {$istart + $clen - [tell $ifd]}] + if {$c > 4096} { set c 4096 } + if {$c <= 0} { + break + } + seek $ifd $tell start + set data [read $ifd $c] + set tell [tell $ifd] + zstream_put $fd $data + while {[string length [set bufdata [zstream_get $fd]]] > 0} { + append buf $bufdata + } + } + } + return $r + } + close - finalize { + eventClean $fd + if {$zcmd != ""} { + rename $zcmd "" + } + unset pos + } + } +} + +proc ::zip::rawstream_handler {ifd ioffset ilen cmd fd {a1 ""} {a2 ""} args} { + upvar ::zip::_rawstream_pos($fd) pos + switch -- $cmd { + initialize { + return [list initialize finalize watch read seek] + } + watch { + eventWatch $fd $a1 + } + seek { + switch $a2 { + 1 - current { incr a1 $pos } + 2 - end { incr a1 $ilen } + } + if {$a1 < 0} {set a1 0} + if {$a1 > $ilen} {set a1 $ilen} + set pos $a1 + return $pos + } + read { + seek $ifd $ioffset + seek $ifd $pos current + set n $a1 + if {$n + $pos > $ilen} { set n [expr {$ilen - $pos}] } + set fc [read $ifd $n] + incr pos [string length $fc] + return $fc + } + close - finalize { + eventClean $fd + unset pos + } + } +} + +proc ::zip::rawstream {ifd ilen} { + set cname _rawstream_[incr ::zip::zseq] + set start [tell $ifd] + set cmd [list ::zip::rawstream_handler $ifd $start $ilen] + if {[catch { + set fd [chan create read $cmd] + }]} { + set fd [rechan $cmd 2] + } + set ::zip::_rawstream_pos($fd) 0 + return $fd +} +