File: tar.rkt

package info (click to toggle)
racket 7.9%2Bdfsg1-2
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 178,684 kB
  • sloc: ansic: 282,112; lisp: 234,887; pascal: 70,954; sh: 27,112; asm: 16,268; makefile: 4,613; cpp: 2,715; ada: 1,681; javascript: 1,244; cs: 879; exp: 499; csh: 422; python: 274; xml: 106; perl: 104
file content (311 lines) | stat: -rw-r--r-- 13,848 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
#lang racket/base
(require file/gzip racket/file)

(define tar-block-size 512)
(define tar-name-length 100)
(define tar-prefix-length 155)
(define tar-link-name-length 100)

(define 0-block (make-bytes tar-block-size 0)) ; used for fast block zeroing

(define (new-block) (bytes-copy 0-block))
(define (zero-block! buf) (bytes-copy! buf 0 0-block))

(define path->name-bytes
  (case (system-type)
    [(windows) (lambda (p) (regexp-replace* #rx"\\\\" (path->bytes p) "/"))]
    [else path->bytes]))

(define sep-char (char->integer #\/))

(define (split-tar-name path)
  (let* ([bts (path->name-bytes path)]
         [len (bytes-length bts)])
    (if (< len tar-name-length)
        (values bts #f #t)
        (let loop ([n 1]) ; search for a split point
          (cond [(<= (sub1 len) n)
                 ;; Doesn't fit, so we'll use an extension record:
                 (values (subbytes bts 0 (sub1 tar-name-length)) #f #f)]
                [(and (eq? sep-char (bytes-ref bts n))
                      (< n tar-prefix-length)
                      (< (- len (+ n 1)) tar-name-length))
                 ;; Fits after splitting:
                 (values (subbytes bts (add1 n)) (subbytes bts 0 n) #t)]
                [else (loop (add1 n))])))))

;; see also the same function name in "zip.rkt"
(define (path-attributes path dir?)
  (apply bitwise-ior
         (map (lambda (p)
                (case p
                  [(read)    #o444]
                  [(write)   #o200] ; mask out write bits
                  [(execute) #o111]))
              (file-or-directory-permissions path))))

(define 0-byte (char->integer #\0))

(define ((tar-one-entry buf prefix get-timestamp follow-links? format) path)
  (let* ([link?   (and (not follow-links?) (link-exists? path))]
         [dir?    (and (not link?) (directory-exists? path))]
         [size    (if (or dir? link?) 0 (file-size path))]
         [p       0] ; write pointer
         [cksum   0]
         [cksum-p #f])
    (define full-file-name (if prefix
                               (build-path prefix path)
                               path))
    (define-values (file-name file-prefix file-name-fits?)
      (split-tar-name full-file-name))
    (define-syntax advance (syntax-rules () [(_ l) (set! p (+ p l))]))
    (define (write-block* len bts) ; no padding required
      (when bts
        (define wlen (min (bytes-length bts) len))
        (bytes-copy! buf p bts 0 wlen)
        (for ([i (in-range wlen)])
          (set! cksum (+ cksum (bytes-ref bts i)))))
      (advance len))
    (define (write-block len bts) ; len includes one nul padding
      (when (and bts (<= len (bytes-length bts)))
        (error 'tar "entry too long, should fit in ~a bytes: ~e"
               (sub1 len) bts))
      (write-block* len bts))
    (define (write-octal len int) ; int should take all space -1 nul-padding
      (let loop ([q (+ p len -2)] [n int])
        (if (< q p)
          (when (< 0 n)
            (error 'tar "integer too big, should fit in ~a bytes: ~e"
                   int (sub1 len)))
          (let ([d (+ 0-byte (modulo n 8))])
            (bytes-set! buf q d)
            (set! cksum (+ cksum d))
            (loop (sub1 q) (quotient n 8)))))
      (advance len))
    (define (write-bytes/fill-block bstr) ; assumes that buf is zero'd
      (write-bytes bstr)
      (define len (bytes-length bstr))
      (unless (zero? (modulo len tar-block-size))
        (write-bytes buf (current-output-port) (modulo len tar-block-size))))
    (define attrib-path
      (if link?
          ;; For a link, use attributes of the containing directory:
          (let-values ([(base name dir?) (split-path path)])
            (or (and (path? base)
                     base)
                (current-directory)))
          path))
    (define link-path-bytes (and link?
                                 (path->bytes (resolve-path path))))
    ;; see http://www.mkssoftware.com/docs/man4/tar.4.asp for format spec
    (define (write-a-block file-name-bytes file-prefix size type link-path-bytes)
      (set! p 0)
      (set! cksum 0)
      (set! cksum-p #f)
      (write-block tar-name-length file-name-bytes)
      (write-octal   8 (path-attributes attrib-path dir?))
      (write-octal   8 0)          ; always root (uid)
      (write-octal   8 0)          ; always root (gid)
      (write-octal  12 size)
      (write-octal  12 (get-timestamp attrib-path))
      ;; set checksum later, consider it "all blanks" for cksum
      (set! cksum-p p) (set! cksum (+ cksum (* 8 32))) (advance 8)
      (write-block*  1 type) ; type-flag
      (if link-path-bytes
          (let ([len (min (sub1 tar-link-name-length) (bytes-length link-path-bytes))])
            (write-block* len link-path-bytes)   ; link-name (possibly truncated)
            (advance (- tar-link-name-length len)))
          (advance tar-link-name-length))        ; no link-name
      (write-block   6 #"ustar")   ; magic
      (write-block*  2 #"00")      ; version
      (write-block  32 #"root")    ; always root (user-name)
      (write-block  32 #"root")    ; always root (group-name)
      (write-octal   8 0)          ; device-major
      (write-octal   8 0)          ; device-minor
      (write-block tar-prefix-length file-prefix)
      (set! p cksum-p)
      (write-octal   8 cksum)      ; patch checksum
      (write-bytes buf))
    ;; If the file name is too long, then we need to write an
    ;; extension block, first (GNU format):
    (when (and (not file-name-fits?)
               (eq? format 'gnu))
      (define full-file-bytes (path->bytes full-file-name))
      (write-a-block #"././@LongLink" #"" (bytes-length full-file-bytes) #"L" #f)
      (zero-block! buf)
      (write-bytes/fill-block full-file-bytes))
    ;; Ditto for a long link target (GNU format):
    (define long-target-link? (and link-path-bytes
                                   ((bytes-length link-path-bytes) . >= . tar-link-name-length)))
    (when (and long-target-link?
               (eq? format 'gnu))
      (write-a-block #"././@LongLink" #"" (bytes-length link-path-bytes) #"K" #f)
      (zero-block! buf)
      (write-bytes/fill-block link-path-bytes))
    ;; Or this way (pax format):
    (when (and (or (not file-name-fits?)
                   long-target-link?)
               (eq? format 'pax))
      (define full-file-bytes (path->bytes full-file-name))
      (define pax-bytes
        (let* ([ht #hash()]
               [ht (if (not file-name-fits?) (hash-set ht 'path full-file-bytes) ht)]
               [ht (if long-target-link? (hash-set ht 'linkpath link-path-bytes) ht)])
          (pax->bytes ht)))
      (define pax-header-path-bytes (let ([bstr (bytes-append #"PaxHeader/" full-file-bytes)])
                                      (if ((bytes-length bstr) . >= . tar-name-length)
                                          (subbytes bstr 0 (sub1 tar-name-length))
                                          bstr)))
      (write-a-block pax-header-path-bytes #"" (bytes-length pax-bytes) #"x" #f)
      (zero-block! buf)
      (write-bytes/fill-block pax-bytes))
    ;; If plain 'ustar, report an error for long paths
    (when (eq? format 'ustar)
      (when (not file-name-fits?)
        (error 'tar "path too long for ustar, must fit in ~a bytes: ~e"
               (sub1 tar-name-length)
               full-file-name))
      (when long-target-link?
        (error 'tar "symbolic-link target too long for ustar, must fit in ~a bytes: ~e"
               (sub1 tar-link-name-length)
               link-path-bytes)))
    ;; Write the data block
    (write-a-block file-name file-prefix size (if link? #"2" (if dir? #"5" #"0")) link-path-bytes)
    ;; Write the file data (if any)
    (if (or dir? link?)
      (zero-block! buf) ; must clean buffer for re-use
      ;; write the file
      (with-input-from-file path
        (lambda ()
          (let loop ([n size])
            (let ([l (read-bytes! buf)])
              (cond
                [(eq? l tar-block-size) (write-bytes buf) (loop (- n l))]
                [(number? l) ; shouldn't happen
                 (write-bytes buf (current-output-port) 0 l) (loop (- n l))]
                [(not (eq? eof l)) (error 'tar "internal error")]
                [(not (zero? n))
                 (error 'tar "file changed while packing: ~e" path)]
                [else (zero-block! buf) ; must clean buffer for re-use
                      (let ([l (modulo size tar-block-size)])
                        (unless (zero? l)
                          ;; complete block (buf is now zeroed)
                          (write-bytes buf (current-output-port)
                                       0 (- tar-block-size l))))]))))))))

;; tar-write : (listof relative-path) ->
;; writes a tar file to current-output-port
(provide tar->output)
(define (tar->output files [out (current-output-port)]
                     #:timestamp [timestamp #f]
                     #:get-timestamp [get-timestamp (if timestamp
                                                        (lambda (p) timestamp)
                                                        file-or-directory-modify-seconds)]
                     #:path-prefix [prefix #f]
                     #:follow-links? [follow-links? #f]
                     #:format [format 'pax])
  (check-format 'tar->output format)
  (parameterize ([current-output-port out])
    (let* ([buf (new-block)] [entry (tar-one-entry buf prefix get-timestamp follow-links? format)])
      (for-each entry files)
      ;; two null blocks end-marker
      (write-bytes buf) (write-bytes buf))))

;; tar : output-file paths ->
(provide tar)
(define (tar tar-file
             #:exists-ok? [exists-ok? #f]
             #:path-prefix [prefix #f]
             #:path-filter [path-filter #f]
             #:follow-links? [follow-links? #f]
             #:timestamp [timestamp #f]
             #:get-timestamp [get-timestamp (if timestamp
                                                (lambda (p) timestamp)
                                                file-or-directory-modify-seconds)]
             #:format [format 'pax]
             . paths)
  (check-format 'tar format)
  (when (null? paths) (error 'tar "no paths specified"))
  (with-output-to-file tar-file
    #:exists (if exists-ok? 'truncate/replace 'error)
    (lambda () (tar->output (pathlist-closure paths
                                         #:follow-links? follow-links?
                                         #:path-filter path-filter)
                       #:get-timestamp get-timestamp
                       #:path-prefix prefix
                       #:follow-links? follow-links?
                       #:format format))))

;; tar-gzip : output-file paths ->
(provide tar-gzip)
(define (tar-gzip tgz-file
                  #:exists-ok? [exists-ok? #f]
                  #:path-prefix [prefix #f]
                  #:path-filter [path-filter #f]
                  #:follow-links? [follow-links? #f]
                  #:timestamp [timestamp #f]
                  #:get-timestamp [get-timestamp (if timestamp
                                                     (lambda (p) timestamp)
                                                     file-or-directory-modify-seconds)]
                  #:format [format 'pax]
                  . paths)
  (check-format 'tar-gzip format)
  (when (null? paths) (error 'tar-gzip "no paths specified"))
  (with-output-to-file tgz-file
    #:exists (if exists-ok? 'truncate/replace 'error)
    (lambda ()
      (let-values ([(i o) (make-pipe (* 1024 1024 32))])
        (define tar-exn #f)
        (thread (lambda ()
                  (with-handlers [((lambda (exn) #t) (lambda (exn) (set! tar-exn exn)))]
                    (tar->output (pathlist-closure paths
                                                   #:follow-links? follow-links?
                                                   #:path-filter path-filter)
                                 o
                                 #:path-prefix prefix
                                 #:follow-links? follow-links?
                                 #:get-timestamp get-timestamp
                                 #:format format))
                  (close-output-port o)))
        (gzip-through-ports
         i (current-output-port)
         (cond [(regexp-match #rx"^(.*[.])(?:tar[.]gz|tgz)$"
                              (if (path? tgz-file)
                                (path->string tgz-file) tgz-file))
                => (lambda (m) (string-append (car m) "tar"))]
               [else #f])
         (current-seconds))
        (when tar-exn (raise tar-exn))))))

;; ----------------------------------------

(define (check-format who format)
  (case format
    [(ustar gnu pax) (void)]
    [else (raise-argument-error who "(or/c 'pax 'gnu 'ustar)" format)]))

(define (pax->bytes ht)
  (define (number->bytes n) (string->bytes/utf-8 (format "~a" n)))
  (apply
   bytes-append
   (for/list ([(k v) (in-hash ht)])
     (define k-bytes (string->bytes/utf-8 (symbol->string k)))
     (define len
       ;; Weird: we have to figure out the line length *including* the bytes
       ;; that describe the line length.
       (let loop ([guess-len (+ 1 (bytes-length k-bytes) 1 (bytes-length v) 1)])
         (define n-bytes (number->bytes guess-len))
         (define encoding-size (+ (bytes-length n-bytes) 1 (bytes-length k-bytes) 1 (bytes-length v) 1))
         (cond
          [(= guess-len encoding-size)
           guess-len]
          [(guess-len . < . encoding-size)
           (loop (add1 guess-len))]
          [else
           (error 'pax->bytes "internal error: cannot figure out encoding line length!")])))
     (bytes-append (number->bytes len)
                   #" "
                   k-bytes
                   #"="
                   v
                   #"\n"))))