summaryrefslogtreecommitdiffstats
path: root/tcllib/modules/tar/tar.tcl
diff options
context:
space:
mode:
Diffstat (limited to 'tcllib/modules/tar/tar.tcl')
-rw-r--r--tcllib/modules/tar/tar.tcl540
1 files changed, 540 insertions, 0 deletions
diff --git a/tcllib/modules/tar/tar.tcl b/tcllib/modules/tar/tar.tcl
new file mode 100644
index 0000000..2892896
--- /dev/null
+++ b/tcllib/modules/tar/tar.tcl
@@ -0,0 +1,540 @@
+# tar.tcl --
+#
+# Creating, extracting, and listing posix tar archives
+#
+# Copyright (c) 2004 Aaron Faupell <afaupell@users.sourceforge.net>
+# Copyright (c) 2013 Andreas Kupries <andreas_kupries@users.sourceforge.net>
+# (GNU tar @LongLink support).
+#
+# See the file "license.terms" for information on usage and redistribution
+# of this file, and for a DISCLAIMER OF ALL WARRANTIES.
+#
+# RCS: @(#) $Id: tar.tcl,v 1.17 2012/09/11 17:22:24 andreas_kupries Exp $
+
+package require Tcl 8.4
+package provide tar 0.10
+
+namespace eval ::tar {}
+
+proc ::tar::parseOpts {acc opts} {
+ array set flags $acc
+ foreach {x y} $acc {upvar $x $x}
+
+ set len [llength $opts]
+ set i 0
+ while {$i < $len} {
+ set name [string trimleft [lindex $opts $i] -]
+ if {![info exists flags($name)]} {return -code error "unknown option \"$name\""}
+ if {$flags($name) == 1} {
+ set $name [lindex $opts [expr {$i + 1}]]
+ incr i $flags($name)
+ } elseif {$flags($name) > 1} {
+ set $name [lrange $opts [expr {$i + 1}] [expr {$i + $flags($name)}]]
+ incr i $flags($name)
+ } else {
+ set $name 1
+ }
+ incr i
+ }
+}
+
+proc ::tar::pad {size} {
+ set pad [expr {512 - ($size % 512)}]
+ if {$pad == 512} {return 0}
+ return $pad
+}
+
+proc ::tar::seekorskip {ch off wh} {
+ if {[tell $ch] < 0} {
+ if {$wh!="current"} {
+ error "WHENCE=$wh not supported on non-seekable channel $ch"
+ }
+ skip $ch $off
+ return
+ }
+ seek $ch $off $wh
+ return
+}
+
+proc ::tar::skip {ch skipover} {
+ while {$skipover > 0} {
+ set requested $skipover
+
+ # Limit individual skips to 64K, as a compromise between speed
+ # of skipping (Number of read requests), and memory usage
+ # (Note how skipped block is read into memory!). While the
+ # read data is immediately discarded it still generates memory
+ # allocation traffic, gets copied, etc. Trying to skip the
+ # block in one go without the limit may cause us to run out of
+ # (virtual) memory, or just induce swapping, for nothing.
+
+ if {$requested > 65536} {
+ set requested 65536
+ }
+
+ set skipped [string length [read $ch $requested]]
+
+ # Stop in short read into the end of the file.
+ if {!$skipped && [eof $ch]} break
+
+ # Keep track of how much is (not) skipped yet.
+ incr skipover -$skipped
+ }
+ return
+}
+
+proc ::tar::readHeader {data} {
+ binary scan $data a100a8a8a8a12a12a8a1a100a6a2a32a32a8a8a155 \
+ name mode uid gid size mtime cksum type \
+ linkname magic version uname gname devmajor devminor prefix
+
+ foreach x {name type linkname} {
+ set $x [string trim [set $x] "\x00"]
+ }
+ foreach x {uid gid size mtime cksum} {
+ set $x [format %d 0[string trim [set $x] " \x00"]]
+ }
+ set mode [string trim $mode " \x00"]
+
+ if {$magic == "ustar "} {
+ # gnu tar
+ # not fully supported
+ foreach x {uname gname prefix} {
+ set $x [string trim [set $x] "\x00"]
+ }
+ foreach x {devmajor devminor} {
+ set $x [format %d 0[string trim [set $x] " \x00"]]
+ }
+ } elseif {$magic == "ustar\x00"} {
+ # posix tar
+ foreach x {uname gname prefix} {
+ set $x [string trim [set $x] "\x00"]
+ }
+ foreach x {devmajor devminor} {
+ set $x [format %d 0[string trim [set $x] " \x00"]]
+ }
+ } else {
+ # old style tar
+ foreach x {uname gname devmajor devminor prefix} { set $x {} }
+ if {$type == ""} {
+ if {[string match */ $name]} {
+ set type 5
+ } else {
+ set type 0
+ }
+ }
+ }
+
+ return [list name $name mode $mode uid $uid gid $gid size $size mtime $mtime \
+ cksum $cksum type $type linkname $linkname magic $magic \
+ version $version uname $uname gname $gname devmajor $devmajor \
+ devminor $devminor prefix $prefix]
+}
+
+proc ::tar::contents {file args} {
+ set chan 0
+ parseOpts {chan 0} $args
+ if {$chan} {
+ set fh $file
+ } else {
+ set fh [::open $file]
+ fconfigure $fh -encoding binary -translation lf -eofchar {}
+ }
+ set ret {}
+ while {![eof $fh]} {
+ array set header [readHeader [read $fh 512]]
+ HandleLongLink $fh header
+ if {$header(name) == ""} break
+ if {$header(prefix) != ""} {append header(prefix) /}
+ lappend ret $header(prefix)$header(name)
+ seekorskip $fh [expr {$header(size) + [pad $header(size)]}] current
+ }
+ if {!$chan} {
+ close $fh
+ }
+ return $ret
+}
+
+proc ::tar::stat {tar {file {}} args} {
+ set chan 0
+ parseOpts {chan 0} $args
+ if {$chan} {
+ set fh $tar
+ } else {
+ set fh [::open $tar]
+ fconfigure $fh -encoding binary -translation lf -eofchar {}
+ }
+ set ret {}
+ while {![eof $fh]} {
+ array set header [readHeader [read $fh 512]]
+ HandleLongLink $fh header
+ if {$header(name) == ""} break
+ if {$header(prefix) != ""} {append header(prefix) /}
+ seekorskip $fh [expr {$header(size) + [pad $header(size)]}] current
+ if {$file != "" && "$header(prefix)$header(name)" != $file} {continue}
+ set header(type) [string map {0 file 5 directory 3 characterSpecial 4 blockSpecial 6 fifo 2 link} $header(type)]
+ set header(mode) [string range $header(mode) 2 end]
+ lappend ret $header(prefix)$header(name) [list mode $header(mode) uid $header(uid) gid $header(gid) \
+ size $header(size) mtime $header(mtime) type $header(type) linkname $header(linkname) \
+ uname $header(uname) gname $header(gname) devmajor $header(devmajor) devminor $header(devminor)]
+ }
+ if {!$chan} {
+ close $fh
+ }
+ return $ret
+}
+
+proc ::tar::get {tar file args} {
+ set chan 0
+ parseOpts {chan 0} $args
+ if {$chan} {
+ set fh $tar
+ } else {
+ set fh [::open $tar]
+ fconfigure $fh -encoding binary -translation lf -eofchar {}
+ }
+ while {![eof $fh]} {
+ set data [read $fh 512]
+ array set header [readHeader $data]
+ HandleLongLink $fh header
+ if {$header(name) == ""} break
+ if {$header(prefix) != ""} {append header(prefix) /}
+ set name [string trimleft $header(prefix)$header(name) /]
+ if {$name == $file} {
+ set file [read $fh $header(size)]
+ if {!$chan} {
+ close $fh
+ }
+ return $file
+ }
+ seekorskip $fh [expr {$header(size) + [pad $header(size)]}] current
+ }
+ if {!$chan} {
+ close $fh
+ }
+ return {}
+}
+
+proc ::tar::untar {tar args} {
+ set nooverwrite 0
+ set data 0
+ set nomtime 0
+ set noperms 0
+ set chan 0
+ parseOpts {dir 1 file 1 glob 1 nooverwrite 0 nomtime 0 noperms 0 chan 0} $args
+ if {![info exists dir]} {set dir [pwd]}
+ set pattern *
+ if {[info exists file]} {
+ set pattern [string map {* \\* ? \\? \\ \\\\ \[ \\\[ \] \\\]} $file]
+ } elseif {[info exists glob]} {
+ set pattern $glob
+ }
+
+ set ret {}
+ if {$chan} {
+ set fh $tar
+ } else {
+ set fh [::open $tar]
+ fconfigure $fh -encoding binary -translation lf -eofchar {}
+ }
+ while {![eof $fh]} {
+ array set header [readHeader [read $fh 512]]
+ HandleLongLink $fh header
+ if {$header(name) == ""} break
+ if {$header(prefix) != ""} {append header(prefix) /}
+ set name [string trimleft $header(prefix)$header(name) /]
+ if {![string match $pattern $name] || ($nooverwrite && [file exists $name])} {
+ seekorskip $fh [expr {$header(size) + [pad $header(size)]}] current
+ continue
+ }
+
+ set name [file join $dir $name]
+ if {![file isdirectory [file dirname $name]]} {
+ file mkdir [file dirname $name]
+ lappend ret [file dirname $name] {}
+ }
+ if {[string match {[0346]} $header(type)]} {
+ if {[catch {::open $name w+} new]} {
+ # sometimes if we dont have write permission we can still delete
+ catch {file delete -force $name}
+ set new [::open $name w+]
+ }
+ fconfigure $new -encoding binary -translation lf -eofchar {}
+ fcopy $fh $new -size $header(size)
+ close $new
+ lappend ret $name $header(size)
+ } elseif {$header(type) == 5} {
+ file mkdir $name
+ lappend ret $name {}
+ } elseif {[string match {[12]} $header(type)] && $::tcl_platform(platform) == "unix"} {
+ catch {file delete $name}
+ if {![catch {file link [string map {1 -hard 2 -symbolic} $header(type)] $name $header(linkname)}]} {
+ lappend ret $name {}
+ }
+ }
+ seekorskip $fh [pad $header(size)] current
+ if {![file exists $name]} continue
+
+ if {$::tcl_platform(platform) == "unix"} {
+ if {!$noperms} {
+ catch {file attributes $name -permissions 0[string range $header(mode) 2 end]}
+ }
+ catch {file attributes $name -owner $header(uid) -group $header(gid)}
+ catch {file attributes $name -owner $header(uname) -group $header(gname)}
+ }
+ if {!$nomtime} {
+ file mtime $name $header(mtime)
+ }
+ }
+ if {!$chan} {
+ close $fh
+ }
+ return $ret
+}
+
+##
+ # ::tar::statFile
+ #
+ # Returns stat info about a filesystem object, in the form of an info
+ # dictionary like that returned by ::tar::readHeader.
+ #
+ # The mode, uid, gid, mtime, and type entries are always present.
+ # The size and linkname entries are present if relevant for this type
+ # of object. The uname and gname entries are present if the OS supports
+ # them. No devmajor or devminor entry is present.
+ ##
+
+proc ::tar::statFile {name followlinks} {
+ if {$followlinks} {
+ file stat $name stat
+ } else {
+ file lstat $name stat
+ }
+
+ set ret {}
+
+ if {$::tcl_platform(platform) == "unix"} {
+ lappend ret mode 1[file attributes $name -permissions]
+ lappend ret uname [file attributes $name -owner]
+ lappend ret gname [file attributes $name -group]
+ if {$stat(type) == "link"} {
+ lappend ret linkname [file link $name]
+ }
+ } else {
+ lappend ret mode [lindex {100644 100755} [expr {$stat(type) == "directory"}]]
+ }
+
+ lappend ret uid $stat(uid) gid $stat(gid) mtime $stat(mtime) \
+ type $stat(type)
+
+ if {$stat(type) == "file"} {lappend ret size $stat(size)}
+
+ return $ret
+}
+
+##
+ # ::tar::formatHeader
+ #
+ # Opposite operation to ::tar::readHeader; takes a file name and info
+ # dictionary as arguments, returns a corresponding (POSIX-tar) header.
+ #
+ # The following dictionary entries must be present:
+ # mode
+ # type
+ #
+ # The following dictionary entries are used if present, otherwise
+ # the indicated default is used:
+ # uid 0
+ # gid 0
+ # size 0
+ # mtime [clock seconds]
+ # linkname {}
+ # uname {}
+ # gname {}
+ #
+ # All other dictionary entries, including devmajor and devminor, are
+ # presently ignored.
+ ##
+
+proc ::tar::formatHeader {name info} {
+ array set A {
+ linkname ""
+ uname ""
+ gname ""
+ size 0
+ gid 0
+ uid 0
+ }
+ set A(mtime) [clock seconds]
+ array set A $info
+ array set A {devmajor "" devminor ""}
+
+ set type [string map {file 0 directory 5 characterSpecial 3 \
+ blockSpecial 4 fifo 6 link 2 socket A} $A(type)]
+
+ set osize [format %o $A(size)]
+ set ogid [format %o $A(gid)]
+ set ouid [format %o $A(uid)]
+ set omtime [format %o $A(mtime)]
+
+ set name [string trimleft $name /]
+ if {[string length $name] > 255} {
+ return -code error "path name over 255 chars"
+ } elseif {[string length $name] > 100} {
+ set common [string range $name end-99 154]
+ if {[set splitpoint [string first / $common]] == -1} {
+ return -code error "path name cannot be split into prefix and name"
+ }
+ set prefix [string range $name 0 end-100][string range $common 0 $splitpoint-1]
+ set name [string range $common $splitpoint+1 end][string range $name 155 end]
+ } else {
+ set prefix ""
+ }
+
+ set header [binary format a100A8A8A8A12A12A8a1a100A6a2a32a32a8a8a155a12 \
+ $name $A(mode)\x00 $ouid\x00 $ogid\x00\
+ $osize\x00 $omtime\x00 {} $type \
+ $A(linkname) ustar\x00 00 $A(uname) $A(gname)\
+ $A(devmajor) $A(devminor) $prefix {}]
+
+ binary scan $header c* tmp
+ set cksum 0
+ foreach x $tmp {incr cksum $x}
+
+ return [string replace $header 148 155 [binary format A8 [format %o $cksum]\x00]]
+}
+
+
+proc ::tar::recurseDirs {files followlinks} {
+ foreach x $files {
+ if {[file isdirectory $x] && ([file type $x] != "link" || $followlinks)} {
+ if {[set more [glob -dir $x -nocomplain *]] != ""} {
+ eval lappend files [recurseDirs $more $followlinks]
+ } else {
+ lappend files $x
+ }
+ }
+ }
+ return $files
+}
+
+proc ::tar::writefile {in out followlinks name} {
+ puts -nonewline $out [formatHeader $name [statFile $in $followlinks]]
+ set size 0
+ if {[file type $in] == "file" || ($followlinks && [file type $in] == "link")} {
+ set in [::open $in]
+ fconfigure $in -encoding binary -translation lf -eofchar {}
+ set size [fcopy $in $out]
+ close $in
+ }
+ puts -nonewline $out [string repeat \x00 [pad $size]]
+}
+
+proc ::tar::create {tar files args} {
+ set dereference 0
+ set chan 0
+ parseOpts {dereference 0 chan 0} $args
+
+ if {$chan} {
+ set fh $tar
+ } else {
+ set fh [::open $tar w+]
+ fconfigure $fh -encoding binary -translation lf -eofchar {}
+ }
+ foreach x [recurseDirs $files $dereference] {
+ writefile $x $fh $dereference $x
+ }
+ puts -nonewline $fh [string repeat \x00 1024]
+
+ if {!$chan} {
+ close $fh
+ }
+ return $tar
+}
+
+proc ::tar::add {tar files args} {
+ set dereference 0
+ set prefix ""
+ set quick 0
+ parseOpts {dereference 0 prefix 1 quick 0} $args
+
+ set fh [::open $tar r+]
+ fconfigure $fh -encoding binary -translation lf -eofchar {}
+
+ if {$quick} then {
+ seek $fh -1024 end
+ } else {
+ set data [read $fh 512]
+ while {[regexp {[^\0]} $data]} {
+ array set header [readHeader $data]
+ seek $fh [expr {$header(size) + [pad $header(size)]}] current
+ set data [read $fh 512]
+ }
+ seek $fh -512 current
+ }
+
+ foreach x [recurseDirs $files $dereference] {
+ writefile $x $fh $dereference $prefix$x
+ }
+ puts -nonewline $fh [string repeat \x00 1024]
+
+ close $fh
+ return $tar
+}
+
+proc ::tar::remove {tar files} {
+ set n 0
+ while {[file exists $tar$n.tmp]} {incr n}
+ set tfh [::open $tar$n.tmp w]
+ set fh [::open $tar r]
+
+ fconfigure $fh -encoding binary -translation lf -eofchar {}
+ fconfigure $tfh -encoding binary -translation lf -eofchar {}
+
+ while {![eof $fh]} {
+ array set header [readHeader [read $fh 512]]
+ if {$header(name) == ""} {
+ puts -nonewline $tfh [string repeat \x00 1024]
+ break
+ }
+ if {$header(prefix) != ""} {append header(prefix) /}
+ set name $header(prefix)$header(name)
+ set len [expr {$header(size) + [pad $header(size)]}]
+ if {[lsearch $files $name] > -1} {
+ seek $fh $len current
+ } else {
+ seek $fh -512 current
+ fcopy $fh $tfh -size [expr {$len + 512}]
+ }
+ }
+
+ close $fh
+ close $tfh
+
+ file rename -force $tar$n.tmp $tar
+}
+
+proc ::tar::HandleLongLink {fh hv} {
+ upvar 1 $hv header thelongname thelongname
+
+ # @LongName Part I.
+ if {$header(type) == "L"} {
+ # Size == Length of name. Read it, and pad to full 512
+ # size. After that is a regular header for the actual
+ # file, where we have to insert the name. This is handled
+ # by the next iteration and the part II below.
+ set thelongname [string trimright [read $fh $header(size)] \000]
+ seekorskip $fh [pad $header(size)] current
+ return -code continue
+ }
+ # Not supported yet: type 'K' for LongLink (long symbolic links).
+
+ # @LongName, part II, get data from previous entry, if defined.
+ if {[info exists thelongname]} {
+ set header(name) $thelongname
+ # Prevent leakage to further entries.
+ unset thelongname
+ }
+
+ return
+}