You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

196 lines
7.3 KiB

# ZIP file constructor
package provide zipper 999999.0a1.0
namespace eval zipper {
namespace export initialize addentry adddir finalize
namespace eval v {
variable fd
variable base
variable toc
}
#if we initialize before writing anything to fd - our base is the file base
# - ie we get an 'internal preamble'
#if instead, we write data to fd before initialize, our base is the start of the archive-data.
# - ie we get an 'external preamble'
#Either way can work - but some zip utilities expect the base to always be the start of the file,
#others are able to process the external preamble.
#If the filename has the .zip extension - there should be no external preamble
#(utils may follow a different codepath for files with different extensions)
#
#It seems to be ok either way for reading - but some tools cannot write to file based offset if there is prefix data
#(e.g file.kit with offset adjusted with something like zip -A which makes the preamble internal to the zip)
# and some cannot write to archive-based offset if there is prefix data !
#(e.g file.kit with preamble prepended and offsets not adjusted = external preamble)
#
#Some tools may auto-adjust to file-based offset when adding entries (e.g pkzip if extension is .zip)
proc initialize {fd} {
set v::fd $fd
set v::base [tell $fd]
set v::toc {}
#fconfigure $fd -translation binary -encoding binary
fconfigure $fd -translation binary -encoding iso8859-1
}
proc emit {s} {
puts -nonewline $v::fd $s
}
proc dostime {sec {gmt 0}} {
set f [clock format $sec -format {%Y %m %d %H %M %S} -gmt $gmt]
regsub -all { 0(\d)} $f { \1} f
foreach {Y M D h m s} $f break
set date [expr {(($Y-1980)<<9) | ($M<<5) | $D}]
set time [expr {($h<<11) | ($m<<5) | ($s>>1)}]
return [list $date $time]
}
proc addentry {name contents {unixmtime ""} {force 0}} {
if {$unixmtime == ""} { set unixmtime [clock seconds] }
#lassign [dostime $date 1] date time ;#UTC would probably be more sensible - but convention seems to be localtime :/
lassign [dostime $unixmtime 0] date time
set flag 0
set type 0 ;# stored
set fsize [string length $contents]
set csize $fsize
set fnlen [string length $name]
if {$force > 0 && $force != [string length $contents]} {
set csize $fsize
set fsize $force
set type 8 ;# if we're passing in compressed data, it's deflated
}
if {[catch { zlib crc32 $contents } crc]} {
set crc 0
} elseif {$type == 0} {
set cdata [zlib deflate $contents]
if {[string length $cdata] < [string length $contents]} {
set contents $cdata
set csize [string length $cdata]
set type 8 ;# deflate
}
}
#we are at the position to write a *local* file header (record including file data, and often with some duplication of data in corresponding CDR 'file header' - prior to CDR records)
#use the position to calculate the offset for the corresponding CDR file header
# -- --- --- --- --- --- ---
set local_file_relative_offset [expr {[tell $v::fd] -$v::base}]
#toc / File header within Central directory structure
#PK\1\2 - 0x02014b50
#lappend v::toc "[binary format a2c6ssssiiiss4ii PK {1 2 20 0 20 0} \
# $flag $type $time $date $crc $csize $fsize $fnlen \
# {0 0 0 0} 128 [tell $v::fd]]$name"
#build the CDR file header - but we don't add it here
set do_extended_timestamp 1
if {!$do_extended_timestamp} {
lappend v::toc "[binary format a2c6ssssiiiss4ii PK {1 2 20 0 20 0} \
$flag $type $time $date $crc $csize $fsize $fnlen \
{0 0 0 0} 128 $local_file_relative_offset]$name"
} else {
set extra ""
# ---
# Value Size Description
# ----- ---- -----------
#(time) 0x5455 Short tag for this extra block type ("UT")
# TSize Short total data size for this block
# Flags Byte info bits (refers to local header!)
# (ModTime) Long time of last modification (UTC/GMT)
# ---
# - Tsize = 9 - 4 = 5
set extended_timestamp [binary format a2sci UT 5 0 $unixmtime]
append extra $extended_timestamp
# ---
set extralen [string length $extra]
lappend v::toc "[binary format a2c6ssssiiisss3ii PK {1 2 20 0 20 0} \
$flag $type $time $date $crc $csize $fsize $fnlen \
$extralen {0 0 0} 128 $local_file_relative_offset]$name$extra"
}
# -- --- --- --- --- --- ---
#*Local* File Header PK\3\4 = 0x04034b50 (this is outside of and prior to CDR)
emit [binary format a2c4ssssiiiss PK {3 4 20 0} \
$flag $type $time $date $crc $csize $fsize $fnlen 0]
emit $name
emit $contents
}
proc adddir {name {date ""} {force 0}} {
set name "${name}/"
if {$date == ""} { set date [clock seconds] }
lassign [dostime $date 0] date time
set flag 0
set type 0 ;# stored
set fsize 0
set csize 0
set fnlen [string length $name]
set crc 0
lappend v::toc "[binary format a2c6ssssiiiss4ii PK {1 2 20 0 20 0} \
$flag $type $time $date $crc $csize $fsize $fnlen \
{0 0 0 0} 128 [tell $v::fd]]$name"
emit [binary format a2c4ssssiiiss PK {3 4 20 0} \
$flag $type $time $date $crc $csize $fsize $fnlen 0]
emit $name
}
proc finalize {} {
set cd_start_pos [tell $v::fd]
set ntoc [llength $v::toc]
foreach x $v::toc { emit $x }
set v::toc {}
set cd_end_pos [tell $v::fd]
set len [expr {$cd_end_pos - $cd_start_pos}]
#incr pos -$v::base
set cdr_offset_pos [expr $cd_start_pos -$v::base] ;#review
#EOCD signature PK\5\6 = 0x06054b50
emit [binary format a2c2ssssiis PK {5 6} 0 0 $ntoc $ntoc $len $cdr_offset_pos 0]
return $v::fd
}
}
if {[info exists pkgtest] && $pkgtest} {
puts "no test code"
}
# test code below runs when this is launched as the main script
if {[info exists argv0] && [string match zipper-* [file tail $argv0]]} {
#2024 - zlib should generally be builtin..
catch { package require zlib }
zipper::initialize [open try.zip w]
set dirs [list .]
while {[llength $dirs] > 0} {
set d [lindex $dirs 0]
set dirs [lrange $dirs 1 end]
foreach f [lsort [glob -nocomplain [file join $d *]]] {
if {[file isfile $f]} {
regsub {^\./} $f {} f
set fd [open $f]
fconfigure $fd -translation binary -encoding binary
zipper::addentry $f [read $fd] [file mtime $f]
close $fd
} elseif {[file isdir $f]} {
lappend dirs $f
}
}
}
close [zipper::finalize]
puts "size = [file size try.zip]"
puts [exec unzip -v try.zip]
file delete try.zip
}