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
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 |
|
}
|
|
|