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
let ( let* ) = Result.bind
let rec safe f a =
try Ok (f a) with
| Unix.Unix_error (Unix.EINTR, _, _) -> safe f a
| Unix.Unix_error (e, f, s) -> Error (`Unix (e, f, s))
let safe_close fd =
try Unix.close fd with _ -> ()
let read_complete fd buf len =
let rec loop offset =
if offset < len then
let* n = safe (Unix.read fd buf offset) (len - offset) in
if n = 0 then
Error `Unexpected_end_of_file
else
loop (offset + n)
else
Ok ()
in
loop 0
let seek fd n =
safe (Unix.lseek fd n) Unix.SEEK_CUR
|> Result.map ignore
type error = [
| `Fatal of Tar.error
| `Unix of Unix.error * string * string
| `Unexpected_end_of_file
| `Msg of string
]
let pp_error ppf = function
| `Fatal err -> Tar.pp_error ppf err
| `Unix (err, fname, arg) ->
Format.fprintf ppf "Unix error %s (function %s, arg %s)"
(Unix.error_message err) fname arg
| `Unexpected_end_of_file ->
Format.fprintf ppf "Unexpected end of file"
| `Msg msg ->
Format.fprintf ppf "Error %s" msg
module High : sig
type t
type 'a s = 'a
external inj : 'a s -> ('a, t) Tar.io = "%identity"
external prj : ('a, t) Tar.io -> 'a s = "%identity"
end = struct
type t
type 'a s = 'a
external inj : 'a -> 'b = "%identity"
external prj : 'a -> 'b = "%identity"
end
type t = High.t
let value v = Tar.High (High.inj v)
let run t fd =
let rec run : type a. (a, _ as 'err, t) Tar.t -> (a, 'err) result = function
| Tar.Write str ->
let* _write = safe (Unix.write_substring fd str 0) (String.length str) in
Ok ()
| Tar.Read len ->
let b = Bytes.make len '\000' in
let* read = safe (Unix.read fd b 0) len in
if read = 0 then
Error `Unexpected_end_of_file
else if len = read then
Ok (Bytes.unsafe_to_string b)
else
Ok (Bytes.sub_string b 0 read)
| Tar.Really_read len ->
let buf = Bytes.make len '\000' in
begin match read_complete fd buf len with
| Ok () -> Ok (Bytes.unsafe_to_string buf)
| Error _ as err -> err end
| Tar.Seek len -> seek fd len
| Tar.Return value -> value
| Tar.High value -> High.prj value
| Tar.Bind (x, f) ->
match run x with
| Ok value -> run (f value)
| Error _ as err -> err in
run t
let fold f filename init =
let* fd = safe Unix.(openfile filename [ O_RDONLY ]) 0 in
Fun.protect
~finally:(fun () -> safe_close fd)
(fun () -> run (Tar.fold f init) fd)
let unix_err_to_msg = function
| `Unix (e, f, s) ->
`Msg (Format.sprintf "error %s in function %s %s"
(Unix.error_message e) f s)
let copy ~dst_fd len =
let blen = 65536 in
let rec read_write ~dst_fd len =
let open Tar.Syntax in
if len = 0 then Tar.return (Ok ())
else
let slen = min blen len in
let* str = Tar.really_read (min blen len) in
safe (Unix.write_substring dst_fd str 0) slen
|> Result.map_error unix_err_to_msg
|> function
| Ok _ -> read_write ~dst_fd (len - slen)
| Error _ as err -> Tar.return err
in
read_write ~dst_fd len
let ?(filter = fun _ -> true) ~src dst =
let f ?global:_ hdr () =
if filter hdr then
match hdr.Tar.Header.link_indicator with
| Tar.Header.Link.Normal ->
begin match Result.map_error unix_err_to_msg
(safe Unix.(openfile (Filename.concat dst hdr.Tar.Header.file_name)
[ O_WRONLY ; O_CREAT ]) hdr.Tar.Header.file_mode) with
| Error _ as err -> Tar.return err
| Ok dst ->
try copy ~dst_fd:dst (Int64.to_int hdr.Tar.Header.file_size)
with exn -> safe_close dst; Tar.return (Error (`Exn exn))
end
| _ ->
let open Tar.Syntax in
let* () = Tar.seek (Int64.to_int hdr.Tar.Header.file_size) in
Tar.return (Ok ())
else
let open Tar.Syntax in
let* () = Tar.seek (Int64.to_int hdr.Tar.Header.file_size) in
Tar.return (Ok ())
in
fold f src ()
(** Return the header needed for a particular file on disk *)
let ?level file =
let level = Tar.Header.compatibility level in
let* stat = safe Unix.LargeFile.lstat file in
let file_mode = stat.Unix.LargeFile.st_perm in
let user_id = stat.Unix.LargeFile.st_uid in
let group_id = stat.Unix.LargeFile.st_gid in
let mod_time = Int64.of_float stat.Unix.LargeFile.st_mtime in
let link_indicator = Tar.Header.Link.Normal in
let link_name = "" in
let* uname =
if level = V7 then
Ok ""
else
try
let* passwd_entry = safe Unix.getpwuid stat.Unix.LargeFile.st_uid in
Ok passwd_entry.Unix.pw_name
with Not_found -> Error (`Msg ("No user entry found for UID"))
in
let devmajor = if level = Ustar then stat.Unix.LargeFile.st_dev else 0 in
let* gname =
if level = V7 then
Ok ""
else
try
let* passwd_entry = safe Unix.getgrgid stat.Unix.LargeFile.st_gid in
Ok passwd_entry.Unix.gr_name
with Not_found -> Error (`Msg "No group entry found for GID")
in
let devminor = if level = Ustar then stat.Unix.LargeFile.st_rdev else 0 in
Ok (Tar.Header.make ~file_mode ~user_id ~group_id ~mod_time ~link_indicator ~link_name
~uname ~gname ~devmajor ~devminor file stat.Unix.LargeFile.st_size)
let write_strings fd datas =
let* _written =
List.fold_left (fun acc d ->
let* _written = acc in
Result.map_error unix_err_to_msg
(safe (Unix.write_substring fd d 0) (String.length d)))
(Ok 0) datas
in
Ok ()
let ?level fd =
let* = Tar.encode_header ?level header in
write_strings fd header_strings
let copy ~src_fd ~dst_fd len =
let blen = 65536 in
let buffer = Bytes.make blen '\000' in
let rec read_write ~src_fd ~dst_fd len =
if len = 0 then Ok ()
else
let l = min blen len in
let* () =
Result.map_error
(function
| `Unix _ as e -> unix_err_to_msg e
| `Unexpected_end_of_file ->
`Msg "Unexpected end of file")
(read_complete src_fd buffer l)
in
let* _written =
Result.map_error unix_err_to_msg
(safe (Unix.write dst_fd buffer 0) l)
in
read_write ~src_fd ~dst_fd (len - l)
in
read_write ~src_fd ~dst_fd len
let append_file ?level ? filename fd =
let* = match header with
| None -> header_of_file ?level filename
| Some x -> Ok x
in
let* () = write_header ?level header fd in
let* src =
Result.map_error unix_err_to_msg
(safe Unix.(openfile filename [ O_RDONLY ]) 0)
in
Fun.protect ~finally:(fun () -> safe_close src)
(fun () -> copy ~src_fd:src ~dst_fd:fd
(Int64.to_int header.Tar.Header.file_size))
let ?level fd =
let* = Tar.encode_global_extended_header ?level header in
write_strings fd header_strings
let write_end fd =
write_strings fd [ Tar.Header.zero_block ; Tar.Header.zero_block ]
let create ?level ?global ?(filter = fun _ -> true) ~src dst =
let* dst_fd =
Result.map_error unix_err_to_msg
(safe Unix.(openfile dst [ O_WRONLY ; O_CREAT ]) 0o644)
in
Fun.protect ~finally:(fun () -> safe_close dst_fd)
(fun () ->
let* () = match global with
| None -> Ok ()
| Some hdr -> write_global_extended_header ?level hdr dst_fd
in
let rec copy_files directory =
let* dir = safe Unix.opendir directory in
Fun.protect ~finally:(fun () -> try Unix.closedir dir with _ -> ())
(fun () ->
let rec next () =
try
let* name = safe Unix.readdir dir in
let filename = Filename.concat directory name in
let* = header_of_file ?level filename in
if filter header then
match header.Tar.Header.link_indicator with
| Normal ->
let* () = append_file ?level ~header filename dst_fd in
next ()
| Directory ->
let* () = copy_files filename in
next ()
| _ -> Ok ()
else Ok ()
with End_of_file -> Ok ()
in
next ())
in
let* () = copy_files src in
write_end dst_fd)