# ACTIVESTATE TEAPOT-PKG BEGIN TM -*- tcl -*- # -- Tcl Module # @@ Meta Begin # Package tar 0.6 # Meta as::build::date 2011-01-19 # Meta as::origin http://sourceforge.net/projects/tcllib # Meta category Tar file handling # Meta description Tar file creation, extraction & manipulation # Meta license BSD # Meta platform tcl # Meta require {Tcl 8.2} # Meta subject {tape archive} archive tar # Meta summary tar # @@ Meta End # ACTIVESTATE TEAPOT-PKG BEGIN REQUIREMENTS package require Tcl 8.2 # ACTIVESTATE TEAPOT-PKG END REQUIREMENTS # ACTIVESTATE TEAPOT-PKG BEGIN DECLARE package provide tar 0.6 # ACTIVESTATE TEAPOT-PKG END DECLARE # ACTIVESTATE TEAPOT-PKG END TM # tar.tcl -- # # Creating, extracting, and listing posix tar archives # # Copyright (c) 2004 Aaron Faupell # # 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.15 2009/12/03 19:59:32 andreas_kupries Exp $ package provide tar 0.6 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::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} { set fh [::open $file] set ret {} while {![eof $fh]} { array set header [readHeader [read $fh 512]] if {$header(name) == ""} break lappend ret $header(prefix)$header(name) seek $fh [expr {$header(size) + [pad $header(size)]}] current } close $fh return $ret } proc ::tar::stat {tar {file {}}} { set fh [::open $tar] set ret {} while {![eof $fh]} { array set header [readHeader [read $fh 512]] if {$header(name) == ""} break seek $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)] } close $fh return $ret } proc ::tar::get {tar file} { set fh [::open $tar] fconfigure $fh -encoding binary -translation lf -eofchar {} while {![eof $fh]} { array set header [readHeader [read $fh 512]] if {$header(name) == ""} break set name [string trimleft $header(prefix)$header(name) /] if {$name == $file} { set file [read $fh $header(size)] close $fh return $file } seek $fh [expr {$header(size) + [pad $header(size)]}] current } close $fh return {} } proc ::tar::untar {tar args} { set nooverwrite 0 set data 0 set nomtime 0 set noperms 0 parseOpts {dir 1 file 1 glob 1 nooverwrite 0 nomtime 0 noperms 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 {} set fh [::open $tar] fconfigure $fh -encoding binary -translation lf -eofchar {} while {![eof $fh]} { array set header [readHeader [read $fh 512]] if {$header(name) == ""} break set name [string trimleft $header(prefix)$header(name) /] if {![string match $pattern $name] || ($nooverwrite && [file exists $name])} { seek $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 {} } } seek $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) } } 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 prefix [string range $name 0 end-100] set name [string range $name end-99 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 parseOpts {dereference 0} $args 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] 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 } 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 }