package opam-repository

  1. Overview
  2. Docs

Source file opamTar.ml

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
(**************************************************************************)
(*                                                                        *)
(*    Copyright 2025-2026 Kate Deplaix                                    *)
(*    Copyright 2026 OCamlPro                                             *)
(*                                                                        *)
(*  All rights reserved. This file is distributed under the terms of the  *)
(*  GNU Lesser General Public License version 2.1, with the special       *)
(*  exception on linking described in the file LICENSE.                   *)
(*                                                                        *)
(**************************************************************************)

open OpamTypes
open Tar.Syntax

type archive = filename
type archived_file = OpamFilename.Unix.t
type archived_file_content = string

let log ?level fmt = OpamConsole.log "TAR" ?level fmt

exception Tar of archive * string
let raise_error tar fmt =
  Printf.ksprintf (fun str -> raise (Tar (tar, str))) fmt

let rec safe_read fd buf off len =
  try Unix.read fd buf off len
  with Unix.Unix_error (Unix.EINTR, _, _) -> safe_read fd buf off len

let run archive =
  let raise_error fmt = raise_error archive fmt in
  let rec run : type a. Unix.file_descr -> (a, _, _) Tar.t -> a = fun fd -> function
    | Tar.Read len ->
      let len = Int64.to_int len in
      let b = Bytes.create len in
      let read = safe_read fd b 0 len in
      if read = 0 then
        raise_error "unexpected end of file"
      else if len = (read : int) then
        Bytes.unsafe_to_string b
      else
        Bytes.sub_string b 0 read
    | Tar.Really_read len ->
      let rec loop fd buf offset len =
        if offset < (len : int) then
          let n = safe_read fd buf offset (len - offset) in
          if n = 0 then
            raise_error "unexpected end of file"
          else
            loop fd buf (offset + n) len
      in
      let len = Int64.to_int len in
      let buf = Bytes.create len in
      loop fd buf 0 len;
      Bytes.unsafe_to_string buf
    | Tar.Return (Ok x) -> x
    | Tar.Return (Error e) ->
      raise_error "%s"
        (match e with
         | `Fatal e -> Format.asprintf "Fatal: %a" Tar.pp_error e
         | `Eof -> "EOF"
         | `Gz s -> "gz: "^s)
    | Tar.High _ | Tar.Write _ | Tar.Seek _ -> assert false
    | Tar.Bind (x, f) -> run fd (f (run fd x))
  in run

let fold_reg_files_aux archive f acc fd =
  let f acc filename content =
    let filename =
      match OpamFilename.Unix.to_relative_canonical filename with
      | Ok filename -> filename
      | Error err ->
        raise_error archive "Path '%s' not allowed: %s"
                    (OpamFilename.Unix.to_string filename) err
    in
    f acc filename content
  in
  let go ?global:_ hdr acc =
    match hdr.Tar.Header.link_indicator with
    | Normal ->
      let* content = Tar.really_read hdr.file_size in
      let acc = f acc (OpamFilename.Unix.of_string hdr.file_name) content in
      Tar.return (Ok acc)
    | Directory -> Tar.return (Ok acc)
    | Hard -> failwith "hardlinks unsupported"
    | Symbolic -> failwith "symlinks unsupported"
    | Character -> failwith "char devices unsupported"
    | Block -> failwith "block devices unsupported"
    | FIFO -> failwith "fifo unsupported"
    | GlobalExtendedHeader -> failwith "global extended header unsupported"
    | PerFileExtendedHeader -> failwith "perfile extended header unsupported"
    | LongLink -> failwith "longlinks unsupported"
    | LongName -> failwith "longnames unsupported"
  in
  run archive fd (Tar_gz.in_gzipped (Tar.fold go acc))

let fold_reg_files f acc archive =
  let fd = Unix.openfile (OpamFilename.to_string archive) [Unix.O_RDONLY] 0 in
  Fun.protect ~finally:(fun () -> Unix.close fd) @@ fun () ->
  fold_reg_files_aux archive f acc fd

module Inplace = struct
  module Map = OpamFilename.Unix.Map
  type t = {
    archive: archive;
    fd : Unix.file_descr;
    content : archived_file_content Map.t;
  }

  let with_open_out archive f =
    let fd = Unix.openfile (OpamFilename.to_string archive) [Unix.O_RDWR] 0o640 in
    Fun.protect ~finally:(fun () -> Unix.close fd) @@ fun () ->
    f {
      archive = archive;
      fd;
      content =
        fold_reg_files_aux archive (fun acc k x -> Map.add k x acc) Map.empty fd
    }

  let fold_reg_files f acc t =
    Map.fold (fun k x acc -> f acc k x) t.content acc

  let exists fname t = Map.mem fname t.content

  let read fname t = Map.find fname t.content

  let add fname content t =
    { t with content = Map.add fname content t.content }

  let mv ~src ~dst t =
    let file_content = read src t in
    let content =
      Map.remove src t.content
      |> Map.add dst file_content
    in
    { t with content }

  let remove fname t =
    { t with content = Map.remove fname t.content }

  let remove_dir dname t =
    let content =
      Map.filter (fun fname _ ->
          not (OpamFilename.Unix.starts_with dname fname))
        t.content
    in
    { t with content }

  let write (t:t) =
    let to_buffer (buf:Buffer.t) tar =
      let rec run : type a. Buffer.t -> (a, 'err, _) Tar.t -> a = fun buf -> function
        | Tar.Write str ->
          Buffer.add_string buf str
        | Tar.Read _ | Tar.Really_read _ | Tar.Seek _ | Tar.High _ ->
          assert false
        | Tar.Return (Ok value) ->
          value
        | Tar.Return (Error e) ->
          raise_error t.archive "%s" (match e with | `Msg e -> e)
        | Tar.Bind (x, f) ->
          run buf (f (run buf x))
      in
      run buf tar
    in
    let entries =
      let dispenser =
        Map.to_seq t.content
        |> Seq.map (fun (path, content) ->
            let path = OpamFilename.Unix.to_string path in
            let hdr =
              Tar.Header.make ~file_mode:0o640 ~mod_time:0L ~user_id:0 ~group_id:0
                path (Int64.of_int (String.length content))
            in
            let data =
              let closed = ref false in
              fun () -> match !closed with
                | false -> closed := true; Tar.return (Ok (Some content))
                | true -> Tar.return (Ok None) in
            Some Tar.Header.Ustar, hdr, data)
        |> OpamCompat.Seq.to_dispenser
      in
      fun () ->
        match dispenser () with
        | None -> Tar.return (Ok None)
        | Some x -> Tar.return (Ok (Some x))
    in
    let tar = Tar.out ~level:Ustar entries in
    let tar = Tar_gz.out_gzipped ~level:4 ~mtime:0l Gz.Unix tar in
    let buf = Buffer.create 10_485_760 in
    to_buffer buf tar;
    let str = Buffer.contents buf in
    let _ : int = Unix.lseek t.fd 0 Unix.SEEK_SET in
    Unix.ftruncate t.fd 0;
    log ~level:3 "Writing archive %s" (OpamFilename.to_string t.archive);
    let _ : int = Unix.write_substring t.fd str 0 (String.length str) in
    ()

end

let create ?(flat=false) ?(except_vcs=false) tar dir =
  log "creating archive %s from %s"
    (OpamFilename.to_string tar)
    (OpamFilename.Dir.to_string dir);
  let fd =
    Unix.openfile (OpamFilename.to_string tar)
      [Unix.O_CREAT; Unix.O_TRUNC; Unix.O_WRONLY] 0o640
  in
  Fun.protect ~finally:(fun () -> Unix.close fd) @@ fun () ->
  let files = OpamFilename.rec_files ~except_vcs dir in
  let content =
    let remove_prefix =
      let dir =
        if flat then dir else OpamFilename.dirname_dir dir
      in
      OpamFilename.remove_prefix dir
    in
    List.fold_left (fun map f ->
        let k = OpamFilename.Unix.of_string (remove_prefix f) in
        Inplace.Map.add k (OpamFilename.read f) map)
      Inplace.Map.empty files
  in
  Inplace.write { archive = tar; fd; content }

module PatchFS = struct
  type root = OpamFilename.t
  module Tar = Inplace
  type file = OpamFilename.Unix.t
  type target = Tar.t
  let root_label = "archive"
  let translate_patch = false
  let root_to_string = OpamFilename.to_string
  let file_to_string = OpamFilename.Unix.to_string
  let equal_file = OpamFilename.Unix.equal
  let get_path ~fail _target file =
  let file = OpamFilename.Unix.of_string file in
    match OpamFilename.Unix.to_relative_canonical file with
    | Ok file -> file
    | Error _ -> fail (); file
  let on_unclean_accept _ _ = ()
  let on_unclean_reject _ _ _ = ()
  let write = Tar.add
  let exists = Tar.exists
  let exists_dir _file _target = false
  let read = Tar.read
  let remove = Tar.remove
  let remove_dir file target =
    Tar.remove_dir (OpamFilename.Unix.dirname file) target
  let same_dirname ~src ~dst =
    OpamFilename.Unix.Dir.equal
      (OpamFilename.Unix.dirname src)
      (OpamFilename.Unix.dirname dst)
  let mv = Tar.mv
  let open_ = Tar.with_open_out
  let save = Tar.write
end

let patch ~allow_unclean patch_source tar =
  OpamPatch.patch (module PatchFS) ~allow_unclean patch_source tar