···11+# container-image - Manage OCI and Docker Images in OCaml
22+33+The `container-image` package provides a straightforward OCaml
44+interface for interacting with OCI and Docker image specifications. It
55+also provide a CLI tool (named `container-image) that allows users to
66+fetch image layers or inspect image contents on your filesystem.
77+88+## Features
99+1010+- [x] An OCaml API to manage OCI and Docker images
1111+- [x] Fetch layers of an OCI or Docker image.
1212+- [ ] Inspect the contents of an image on the local filesystem,
1313+ complete with a git history for easy diff inspection between layers.
1414+1515+## Installation
1616+1717+### From Source
1818+1919+```bash
2020+git clone https://github.com/your-repo/container-image.git
2121+cd container-image
2222+opam install . --deps-only
2323+dune build @install
2424+```
2525+2626+### Using OPAM (When available)
2727+2828+```bash
2929+opam install container-image
3030+```
3131+3232+## Usage
3333+3434+### Fetching Image Layers
3535+3636+To fetch the layers of an image:
3737+3838+```bash
3939+container-image fetch IMAGE_NAME[:TAG]
4040+```
4141+4242+This command downloads the image layers to the current directory. By
4343+default TAG is `latest`.
4444+4545+### Checking Out Image Contents
4646+4747+To inspect an image's contents on the local filesystem:
4848+4949+```bash
5050+container-image checkout [TAG]
5151+```
5252+5353+After running this command, you'll find the image's contents extracted
5454+to the current directory. Importantly, this checkout will include a
5555+git history, allowing you to seamlessly inspect the differences
5656+between layers.
5757+5858+## Documentation
5959+6060+For an in-depth guide on the `container-image` commands and the
6161+underlying OCaml API, check out the [official
6262+documentation](link-to-docs).
6363+6464+## Contributing
6565+6666+Contributions to the `container-image` project are welcome!
6767+6868+## License
6969+7070+This project is licensed under the MIT License. See
7171+[LICENSE](link-to-license-file) for more details.
···11+open Oci_spec
22+module B = Blob
33+44+(* FIXME: code duplication *)
55+let error_msg fmt = Fmt.kstr (fun s -> Error (`Msg s)) fmt
66+77+type task = { promise : unit Eio.Promise.t; resolver : unit Eio.Promise.u }
88+99+type t = {
1010+ root : [ `Dir ] Eio.Path.t;
1111+ lock : Eio.Mutex.t;
1212+ pending : (string, task) Hashtbl.t;
1313+}
1414+1515+let v root = { root; lock = Eio.Mutex.create (); pending = Hashtbl.create 13 }
1616+let ( / ) = Eio.Path.( / )
1717+let mkdirs dir = Eio.Path.mkdirs ~exists_ok:true ~perm:0o700 dir
1818+1919+let mkdir_parent file =
2020+ match Eio.Path.split file with
2121+ | None -> ()
2222+ | Some (parent, _) -> mkdirs parent
2323+2424+let init t =
2525+ mkdirs (t.root / "blobs" / "sha256");
2626+ mkdirs (t.root / "manifests")
2727+2828+let with_lock lock fn =
2929+ Eio.Mutex.lock lock;
3030+ let finally () = Eio.Mutex.unlock lock in
3131+ Fun.protect ~finally fn
3232+3333+let task path = Eio.Path.native_exn path
3434+3535+let remove_task t path =
3636+ let task = task path in
3737+ with_lock t.lock (fun () ->
3838+ match Hashtbl.find_opt t.pending task with
3939+ | None -> ()
4040+ | Some { resolver; _ } ->
4141+ Eio.Promise.resolve resolver ();
4242+ Hashtbl.remove t.pending task)
4343+4444+let find_and_add_task t ?size path =
4545+ let task = task path in
4646+ with_lock t.lock (fun () ->
4747+ match Hashtbl.find_opt t.pending task with
4848+ | Some t -> `Pending t
4949+ | None ->
5050+ let exists = Eio.Path.is_file path in
5151+ let correct_size =
5252+ match size with
5353+ | None -> true
5454+ | Some size ->
5555+ exists && (Eio.Path.stat ~follow:true path).size = size
5656+ in
5757+ if exists && correct_size then `Already_exists
5858+ else
5959+ let p, u = Eio.Promise.create ~label:task () in
6060+ Hashtbl.add t.pending task { promise = p; resolver = u };
6161+ (* if broken file, delete it from the cache *)
6262+ if exists then Eio.Path.unlink path;
6363+ let finally () = remove_task t path in
6464+ `Fresh finally)
6565+6666+let if_exists t ?size ?(then_ = Fun.id) ?(else_ = Fun.id) file =
6767+ match find_and_add_task t ?size file with
6868+ | `Already_exists -> then_ ()
6969+ | `Fresh finally -> Fun.protect ~finally else_
7070+ | `Pending l ->
7171+ Eio.Promise.await l.promise;
7272+ then_ ()
7373+7474+module Blob = struct
7575+ let file t digest =
7676+ let algo = Digest.string_of_algorithm (Digest.algorithm digest) in
7777+ let hash = Digest.encoded_hash digest in
7878+ Eio.Path.(t.root / "blobs" / algo / hash)
7979+8080+ let if_exists t ~size ?then_ ?else_ digest =
8181+ if_exists t ~size ?then_ ?else_ (file t digest)
8282+8383+ let add_fd t digest body =
8484+ Eio.Switch.run @@ fun sw ->
8585+ let file = file t digest in
8686+ mkdir_parent file;
8787+ let dst = Eio.Path.open_out ~sw ~create:(`Exclusive 0o644) file in
8888+ Flow.copy body dst
8989+9090+ let add_string t digest body =
9191+ Eio.Switch.run @@ fun sw ->
9292+ let file = file t digest in
9393+ mkdir_parent file;
9494+ let dst = Eio.Path.open_out ~sw ~create:(`Exclusive 0o644) file in
9595+ let body = Eio.Flow.string_source body in
9696+ try
9797+ Eio.Flow.copy body dst;
9898+ Eio.Flow.close dst
9999+ with e ->
100100+ Eio.Flow.close dst;
101101+ Eio.Path.unlink file;
102102+ raise e
103103+104104+ let get_string t digest =
105105+ let file = file t digest in
106106+ Eio.Path.with_open_in file Eio.Flow.read_all
107107+108108+ let get_fd ~sw t digest =
109109+ let file = file t digest in
110110+ Eio.Path.open_in ~sw file
111111+end
112112+113113+module Manifest = struct
114114+ let file t image =
115115+ let org = Image.org image in
116116+ let name = Image.name image in
117117+ let file str = Eio.Path.(t.root / "manifests" / org / name / str) in
118118+ let tag t = file ("tags/" ^ t) in
119119+ let digest d =
120120+ file
121121+ (Fmt.str "digests/%s/%s"
122122+ Digest.(string_of_algorithm (algorithm d))
123123+ (Digest.encoded_hash d))
124124+ in
125125+ match (Image.tag image, Image.digest image) with
126126+ | None, None -> tag "latest"
127127+ | Some t, None -> tag t
128128+ | _, Some d -> digest d
129129+130130+ let if_exists t ?then_ ?else_ digest =
131131+ if_exists t ?then_ ?else_ (file t digest)
132132+133133+ exception Invalid_descriptor of Image.t * string * string
134134+135135+ let add t image m =
136136+ Eio.Switch.run @@ fun sw ->
137137+ let file = file t image in
138138+ mkdir_parent file;
139139+ let src = Manifest.to_string m in
140140+ let dst = Eio.Path.open_out ~sw ~create:(`Exclusive 0o644) file in
141141+ Eio.Flow.copy_string src dst
142142+143143+ let get t image =
144144+ let file = file t image in
145145+ let str = Eio.Path.with_open_in file Eio.Flow.read_all in
146146+ match Manifest.of_string str with
147147+ | Ok d -> d
148148+ | Error (`Msg e) -> raise (Invalid_descriptor (image, "get/1", e))
149149+150150+ let tags_of_repo dir full_name =
151151+ let tags = Eio.Path.read_dir Eio.Path.(dir / full_name / "tags") in
152152+ List.map (fun tag -> Image.v ~tag full_name) tags
153153+154154+ let digests_of_algo algo dir full_name =
155155+ let digests =
156156+ Eio.Path.read_dir Eio.Path.(dir / full_name / "digests" / algo)
157157+ in
158158+ List.map
159159+ (fun digest ->
160160+ match Digest.algorithm_of_string algo with
161161+ | Error (`Msg e) -> failwith e
162162+ | Ok algo ->
163163+ let digest = Digest.unsafe_v algo digest in
164164+ Image.v ~digest full_name)
165165+ digests
166166+167167+ let digests_of_repo dir full_name =
168168+ let algos = Eio.Path.read_dir Eio.Path.(dir / full_name / "digests") in
169169+ List.map (fun a -> digests_of_algo a dir full_name) algos |> List.flatten
170170+171171+ let map_repo f t =
172172+ let dir = Eio.Path.(t.root / "manifests") in
173173+ let orgs = Eio.Path.read_dir dir in
174174+ let orgs =
175175+ Eio.Fiber.List.map
176176+ (fun org ->
177177+ let names = Eio.Path.read_dir Eio.Path.(dir / org) in
178178+ List.map
179179+ (fun name ->
180180+ let full_name = Fmt.str "%s/%s" org name in
181181+ f dir full_name)
182182+ names)
183183+ orgs
184184+ in
185185+ List.concat (List.concat orgs)
186186+187187+ let list_tags t = map_repo tags_of_repo t
188188+ let list_digests t = map_repo digests_of_repo t
189189+ let list t = list_tags t @ list_digests t
190190+191191+ let guess' t name =
192192+ let digests = list_digests t in
193193+ let matches =
194194+ List.find_all
195195+ (fun i ->
196196+ match Image.digest i with
197197+ | None -> false
198198+ | Some d -> String.starts_with ~prefix:name (Digest.encoded_hash d))
199199+ digests
200200+ in
201201+ match matches with
202202+ | [] -> Image.of_string name
203203+ | [ i ] -> Ok i
204204+ | l ->
205205+ error_msg "%s: ambiguous name; this corresponds to:\n- %a\n" name
206206+ Fmt.(list ~sep:(any "\n- ") Image.pp)
207207+ l
208208+209209+ let guess t name =
210210+ match guess' t name with Ok i -> i | Error (`Msg e) -> invalid_arg e
211211+end
+34
src/cache.mli
···11+open Optint
22+open Oci_spec
33+44+type t
55+66+val v : [ `Dir ] Eio.Path.t -> t
77+val init : t -> unit
88+99+module Blob : sig
1010+ val if_exists :
1111+ t ->
1212+ size:Int63.t ->
1313+ ?then_:(unit -> unit) ->
1414+ ?else_:(unit -> unit) ->
1515+ Digest.t ->
1616+ unit
1717+1818+ val add_fd : t -> Digest.t -> Eio.Flow.source_ty Flow.t -> unit
1919+ val get_fd : sw:Eio.Switch.t -> t -> Digest.t -> Eio.File.ro_ty Eio.Resource.t
2020+ val add_string : t -> Digest.t -> string -> unit
2121+ val get_string : t -> Digest.t -> string
2222+end
2323+2424+module Manifest : sig
2525+ val if_exists :
2626+ t -> ?then_:(unit -> unit) -> ?else_:(unit -> unit) -> Image.t -> unit
2727+2828+ val get : t -> Image.t -> Manifest.t
2929+ val add : t -> Image.t -> Manifest.t -> unit
3030+ val list : t -> Image.t list
3131+ val list_tags : t -> Image.t list
3232+ val list_digests : t -> Image.t list
3333+ val guess : t -> string -> Image.t
3434+end
+123
src/checkout.ml
···11+open Oci_spec
22+33+(* FIXME: code duplication *)
44+let ( let* ) x f = match x with Ok x -> f x | Error e -> Error e
55+let ( let+ ) x f = match x with Ok x -> Ok (f x) | Error e -> Error e
66+let sizes = [| "B"; "KiB"; "MiB"; "GiB"; "TiB"; "PiB"; "EiB"; "ZiB"; "YiB" |]
77+let ( / ) = Eio.Path.( / )
88+let mkdirs dir = Eio.Path.mkdirs ~exists_ok:true ~perm:0o700 dir
99+1010+let mkdir_parent file =
1111+ match Eio.Path.split file with
1212+ | None -> ()
1313+ | Some (parent, _) -> mkdirs parent
1414+1515+let bytes_to_size ?(decimals = 2) ppf = function
1616+ | 0L -> Format.fprintf ppf "0 byte"
1717+ | n ->
1818+ let n = Int64.to_float n in
1919+ let i = Float.floor (Float.log n /. Float.log 1024.) in
2020+ let r = n /. Float.pow 1024. i in
2121+ Format.fprintf ppf "%.*f %s" decimals r sizes.(int_of_float i)
2222+2323+let fold_gzipped f src init =
2424+ Tar_eio.run (Tar_gz.in_gzipped (Tar.fold f init)) src
2525+2626+let checkout_layer ~sw ~cache layer dir =
2727+ let fd = Cache.Blob.get_fd ~sw cache layer in
2828+ Fmt.epr "Extracting layer %a:\n%!" Digest.pp layer;
2929+ let f ?global:_ hdr () =
3030+ let open Tar.Syntax in
3131+ let path = dir / hdr.Tar.Header.file_name in
3232+ mkdir_parent path;
3333+ let file_mode = 0o777 land hdr.Tar.Header.file_mode in
3434+ match hdr.Tar.Header.link_indicator with
3535+ | Directory ->
3636+ Eio.Path.mkdir ~perm:file_mode path;
3737+ Tar.return (Ok ())
3838+ | Symbolic ->
3939+ Eio.Path.symlink ~link_to:hdr.Tar.Header.link_name path;
4040+ Tar.return (Ok ())
4141+ | _ ->
4242+ let file_size = Int64.to_int hdr.Tar.Header.file_size in
4343+ if file_size > 0 then begin
4444+ let* contents = Tar.really_read file_size in
4545+ Eio.Path.save ~create:(`If_missing file_mode) path contents;
4646+ Tar.return (Ok ())
4747+ end
4848+ else begin
4949+ Eio.Path.save ~create:(`If_missing file_mode) path "";
5050+ Tar.return (Ok ())
5151+ end
5252+ in
5353+ match fold_gzipped f (Tar_eio.Flow fd) () with
5454+ | Ok () -> ()
5555+ | Error (`Fatal e) -> Fmt.failwith "Tar error: %a" Tar.pp_error e
5656+ | Error `Eof -> failwith "Unexpected end of file in tar archive"
5757+ | Error (`Gz msg) -> Fmt.failwith "Gzip error: %s" msg
5858+ | Error `Unexpected_end_of_file -> failwith "Unexpected end of file"
5959+6060+let checkout_layers ~sw ~cache ~dir layers =
6161+ List.iteri
6262+ (fun i layer ->
6363+ let dir = Eio.Path.(dir / string_of_int i) in
6464+ let d = Descriptor.digest layer in
6565+ checkout_layer ~sw ~cache d dir)
6666+ layers
6767+6868+let checkout_docker_manifest ~sw ~cache ~dir m =
6969+ checkout_layers ~sw ~cache ~dir (Manifest.Docker.layers m)
7070+7171+let checkout_oci_manifest ~sw ~cache ~dir m =
7272+ checkout_layers ~sw ~cache ~dir (Manifest.OCI.layers m)
7373+7474+let checkout_docker_manifests ~sw ~cache ~dir img ds =
7575+ let ms =
7676+ List.map
7777+ (fun d ->
7878+ let digest = Descriptor.digest d in
7979+ let img = Image.v ~digest img in
8080+ let manifest = Cache.Manifest.get cache img in
8181+ match manifest with
8282+ | `Docker_manifest mani -> mani
8383+ | _ -> failwith "Exptected single docker manifest")
8484+ ds
8585+ in
8686+ List.iteri
8787+ (fun i m ->
8888+ let dir = dir / string_of_int i in
8989+ checkout_docker_manifest ~sw ~cache ~dir m)
9090+ ms
9191+9292+let checkout_oci_manifests ~sw ~cache ~dir ds =
9393+ let ms =
9494+ List.map
9595+ (fun d ->
9696+ let digest = Descriptor.digest d in
9797+ let str = Cache.Blob.get_string cache digest in
9898+ match Manifest.OCI.of_string str with
9999+ | Ok m -> m
100100+ | Error (`Msg e) -> failwith e)
101101+ ds
102102+ in
103103+ List.iteri
104104+ (fun i m ->
105105+ let dir = dir / string_of_int i in
106106+ checkout_oci_manifest ~sw ~cache ~dir m)
107107+ ms
108108+109109+let checkout_docker_manifest_list ~sw ~cache ~dir img l =
110110+ checkout_docker_manifests ~sw ~cache ~dir img (Manifest_list.manifests l)
111111+112112+let checkout_oci_index ~sw ~cache ~dir i =
113113+ checkout_oci_manifests ~sw ~cache ~dir (Index.manifests i)
114114+115115+let checkout ~cache ~root i =
116116+ let dir = root / Image.to_string i in
117117+ Eio.Switch.run @@ fun sw ->
118118+ match Cache.Manifest.get cache i with
119119+ | `Docker_manifest m -> checkout_docker_manifest ~sw ~cache ~dir m
120120+ | `Docker_manifest_list m ->
121121+ checkout_docker_manifest_list ~sw ~cache ~dir (Image.repository i) m
122122+ | `OCI_index i -> checkout_oci_index ~sw ~cache ~dir i
123123+ | `OCI_manifest m -> checkout_oci_manifest ~sw ~cache ~dir m
+151
src/display.ml
···11+module Int63 = Optint.Int63
22+open Oci_spec
33+44+(* FIXME: the None type is probably not needed with switch cancellation *)
55+type t = {
66+ stream : (unit -> unit) option Eio.Stream.t;
77+ display : ((unit -> unit) -> unit, unit) Progress.Display.t;
88+}
99+1010+type line = Int63.t Progress.Line.t
1111+1212+type reporter = {
1313+ stream : (unit -> unit) option Eio.Stream.t;
1414+ reporter : Int63.t Progress.Reporter.t option;
1515+}
1616+1717+let report r i =
1818+ match r.reporter with
1919+ | None -> ()
2020+ | Some reporter ->
2121+ Eio.Stream.add r.stream
2222+ (Some (fun () -> Progress.Reporter.report reporter i))
2323+2424+let report_int r i = report r (Int63.of_int i)
2525+2626+let line ~color ~total message =
2727+ let message = String.sub message 0 (min 21 (String.length message)) in
2828+ let open Progress.Line.Using_int63 in
2929+ list
3030+ [
3131+ rpad 22 (const message);
3232+ bytes;
3333+ bytes_per_sec;
3434+ bar ~color ~style:`UTF8 total;
3535+ percentage_of total ++ const " ";
3636+ ]
3737+3838+let colors =
3939+ let a =
4040+ [
4141+ "#1996f3";
4242+ "#06aeed";
4343+ "#10c6e6";
4444+ "#27dade";
4545+ "#3dead5";
4646+ "#52f5cb";
4747+ "#66fcc2";
4848+ "#7dffb6";
4949+ "#92fda9";
5050+ "#a8f79c";
5151+ "#bced8f";
5252+ "#d2de81";
5353+ "#e8cb72";
5454+ "#feb562";
5555+ "#ff9b52";
5656+ "#ff8143";
5757+ "#ff6232";
5858+ "#ff4121";
5959+ ]
6060+ in
6161+ Array.map Progress.Color.hex (Array.of_list (a @ List.rev a))
6262+6363+let next_color i = colors.(i mod Array.length colors)
6464+6565+let line_of_descriptor d i =
6666+ let total = Descriptor.size d in
6767+ let color = next_color i in
6868+ let txt =
6969+ let digest = Digest.encoded_hash (Descriptor.digest d) in
7070+ let ty =
7171+ match Descriptor.media_type d with
7272+ | Docker Image_manifest_list | OCI Image_index -> "index:"
7373+ | Docker Image_manifest | OCI Image_manifest -> "manifest:"
7474+ | OCI Image_config | Docker Image_config -> "config:"
7575+ | OCI
7676+ ( Layer_tar | Layer_tar_gzip | Layer_tar_zstd
7777+ | Layer_non_distributable_tar | Layer_non_distributable_tar_gzip
7878+ | Layer_non_distributable_tar_zstd )
7979+ | Docker (Layer_tar_gzip | Layer_non_distributable_tar_gzip) ->
8080+ "layer:"
8181+ | Docker Plugin_config -> "plugin:"
8282+ | OCI Trust -> "trust:"
8383+ | _ -> "?:"
8484+ in
8585+ ty ^ digest
8686+ in
8787+ line ~color ~total txt
8888+8989+let line_of_image i n =
9090+ let color = next_color n in
9191+ let image =
9292+ match (Image.tag i, Image.digest i) with
9393+ | None, None -> Image.with_tag "latest" i
9494+ | _ -> i
9595+ in
9696+ let name = Image.to_string image in
9797+ line ~color ~total:(Int63.of_int 100) name
9898+9999+let rec apply_stream ~sw stream =
100100+ Eio.Switch.check sw;
101101+ match Eio.Stream.take stream with
102102+ | Some f ->
103103+ f ();
104104+ apply_stream ~sw stream
105105+ | None -> ()
106106+107107+let init ?platform ~sw image : t =
108108+ let image_name =
109109+ Progress.Line.(
110110+ spacer 4
111111+ ++ constf "🐫 Fetching %a" Fmt.(styled `Bold Image.pp) image
112112+ ++
113113+ match platform with
114114+ | None -> const ""
115115+ | Some p -> constf "%a" Fmt.(styled `Faint (brackets string)) p)
116116+ in
117117+ let stream = Eio.Stream.create max_int in
118118+ let display = Progress.Display.start Progress.Multi.(line image_name) in
119119+ Eio.Fiber.fork ~sw (fun () -> apply_stream ~sw stream);
120120+ { stream; display }
121121+122122+let rec empty_stream stream =
123123+ match Eio.Stream.take_nonblocking stream with
124124+ | None | Some None -> ()
125125+ | Some (Some f) ->
126126+ f ();
127127+ empty_stream stream
128128+129129+let finalise { stream; display } =
130130+ Eio.Stream.add stream None;
131131+ empty_stream stream;
132132+ Progress.Display.finalise display
133133+134134+let lines = ref 0
135135+136136+let with_line ~display ?(show = true) bar f =
137137+ let reporter =
138138+ if show then (
139139+ let r = Progress.Display.add_line display.display (bar !lines) in
140140+ incr lines;
141141+ Some r)
142142+ else None
143143+ in
144144+ let finally () =
145145+ match reporter with
146146+ | None -> ()
147147+ | Some r ->
148148+ Eio.Stream.add display.stream
149149+ (Some (fun () -> Progress.Reporter.finalise r))
150150+ in
151151+ Fun.protect ~finally (fun () -> f { reporter; stream = display.stream })
+16
src/display.mli
···11+open Optint
22+33+type t
44+type line
55+type reporter
66+77+val report : reporter -> Int63.t -> unit
88+val report_int : reporter -> int -> unit
99+val init : ?platform:string -> sw:Eio.Switch.t -> Image.t -> t
1010+val finalise : t -> unit
1111+val line : color:Terminal.Color.t -> total:Int63.t -> string -> line
1212+val line_of_descriptor : Oci_spec.Descriptor.t -> int -> line
1313+val line_of_image : Image.t -> int -> line
1414+1515+val with_line :
1616+ display:t -> ?show:bool -> (int -> line) -> (reporter -> 'b) -> 'b
···11+open Oci_spec
22+open Optint
33+44+(** HTTP client type using GADT for existential type parameters *)
55+type client =
66+ | Client : { net : 'a Eio.Net.t; clock : 'b Eio.Time.clock } -> client
77+88+let create_client ~net ~clock = Client { net; clock }
99+1010+module API = struct
1111+ let registry_base = "https://registry-1.docker.io"
1212+ let auth_base = "https://auth.docker.io"
1313+1414+ type response = {
1515+ content_type : Media_type.t;
1616+ content_length : Int63.t option;
1717+ content_digest : Digest.t option;
1818+ body : Eio.Flow.source_ty Eio.Resource.t;
1919+ }
2020+2121+ let call_get (Client { net; clock }) ~sw ?(accept = []) ?token url out =
2222+ Logs.debug (fun l -> l "GET %s\n%!" url);
2323+ let headers =
2424+ let h = Requests.Headers.empty in
2525+ let h =
2626+ match token with
2727+ | Some token -> Requests.Headers.bearer token h
2828+ | None -> h
2929+ in
3030+ List.fold_left
3131+ (fun h m -> Requests.Headers.add `Accept (Media_type.to_string m) h)
3232+ h accept
3333+ in
3434+ let resp = Requests.One.get ~sw ~clock ~net ~headers url in
3535+ if not (Requests.Response.ok resp) then
3636+ Fmt.failwith "@[<v2>%s error: %d@,%s@]" url
3737+ (Requests.Response.status_code resp)
3838+ (Requests.Response.text resp);
3939+ let content_type =
4040+ match Requests.Response.header_string "Content-Type" resp with
4141+ | Some m -> (
4242+ match Media_type.of_string m with
4343+ | Ok m -> m
4444+ | Error (`Msg e) -> Fmt.failwith "invalid content-type: %s - %s" m e)
4545+ | None -> failwith "missing content-type"
4646+ in
4747+ let content_length =
4848+ match Requests.Response.content_length resp with
4949+ | Some l -> Some (Int63.of_int64 l)
5050+ | None -> None
5151+ in
5252+ let content_digest =
5353+ match Requests.Response.header_string "Docker-Content-Digest" resp with
5454+ | Some s -> (
5555+ match Digest.of_string s with
5656+ | Ok s -> Some s
5757+ | Error (`Msg e) -> Fmt.failwith "%s: invalid digest header: %s" s e)
5858+ | None -> None
5959+ in
6060+ let body = Requests.Response.body resp in
6161+ out { content_length; content_type; content_digest; body }
6262+6363+ let call_post (Client { net; clock }) ~sw url out =
6464+ Logs.debug (fun l -> l "POST %s\n%!" url);
6565+ let resp = Requests.One.post ~sw ~clock ~net url in
6666+ if not (Requests.Response.ok resp) then
6767+ Fmt.failwith "@[<v2>%s error: %d@,%s@]" url
6868+ (Requests.Response.status_code resp)
6969+ (Requests.Response.text resp);
7070+ let body = Requests.Response.body resp in
7171+ out
7272+ {
7373+ content_length = None;
7474+ content_type = OCI Empty;
7575+ content_digest = None;
7676+ body;
7777+ }
7878+7979+ let get client ~sw ?accept ?token url out =
8080+ call_get client ~sw ?accept ?token url out
8181+8282+ let post client ~sw url out = call_post client ~sw url out
8383+8484+ let get_content_length = function
8585+ | None -> failwith "missing content-length headers"
8686+ | Some s -> s
8787+8888+ let get_content_digest = function
8989+ | None -> failwith "missing content-digest headers"
9090+ | Some s -> s
9191+9292+ let manifest_of_fd src =
9393+ match Manifest.of_string (Flow.read_all src) with
9494+ | Ok m -> m
9595+ | Error (`Msg e) -> Fmt.failwith "Fetch.manifest_of_string: %s" e
9696+9797+ let get_manifest client ~progress ~token image =
9898+ Eio.Switch.run @@ fun sw ->
9999+ let name = Image.repository image in
100100+ let reference = Image.reference image in
101101+ let url = Fmt.str "%s/v2/%s/manifests/%s" registry_base name reference in
102102+ let out { content_length; content_type; content_digest; body; _ } =
103103+ let length = get_content_length content_length in
104104+ let digest = get_content_digest content_digest in
105105+ let fd = Flow.source ~progress ~length ~digest body in
106106+ let m = manifest_of_fd fd in
107107+ assert (content_type = Manifest.media_type m);
108108+ m
109109+ in
110110+111111+ let accept =
112112+ Media_type.
113113+ [
114114+ Docker Image_manifest;
115115+ Docker Image_manifest_list;
116116+ OCI Image_index;
117117+ OCI Image_manifest;
118118+ ]
119119+ in
120120+ get client ~accept ~token ~sw url out
121121+122122+ let get_blob client ~sw ~progress ~token image d =
123123+ let size = Descriptor.size d in
124124+ let digest = Descriptor.digest d in
125125+ let name = Image.repository image in
126126+ let url = Fmt.str "%s/v2/%s/blobs/%a" registry_base name Digest.pp digest in
127127+ let out { content_length; content_digest; body; _ } =
128128+ let content_length = get_content_length content_length in
129129+ let () =
130130+ if size <> content_length then failwith "invalid length header";
131131+ match content_digest with
132132+ | None -> ()
133133+ | Some d -> if digest <> d then failwith "invalid digest header"
134134+ in
135135+ Flow.source ~progress ~length:content_length ~digest body
136136+ in
137137+ get client ~sw ~token url out
138138+139139+ type credential = { username : string; password : string }
140140+141141+ let get_token client ?credentials image =
142142+ Eio.Switch.run @@ fun sw ->
143143+ let name = Image.repository image in
144144+ let queries =
145145+ [
146146+ ("service", [ "registry.docker.io" ]);
147147+ ("client_id", [ "image" ]);
148148+ ("scope", [ "repository:" ^ name ^ ":pull" ]);
149149+ ]
150150+ in
151151+ let extra_queries =
152152+ match credentials with
153153+ | None -> []
154154+ | Some { username; password } ->
155155+ [
156156+ ("grant_type", [ "password" ]);
157157+ ("username", [ username ]);
158158+ ("password", [ password ]);
159159+ ]
160160+ in
161161+ let queries = Uri.encoded_of_query (queries @ extra_queries) in
162162+ let url = Fmt.str "%s/token?%s" auth_base queries in
163163+ let out { body; _ } =
164164+ let body = Eio.Flow.read_all body in
165165+ match Auth.of_string body with
166166+ | Ok t -> Auth.token t
167167+ | Error (`Msg e) ->
168168+ Fmt.failwith "@[<v2>%s parsing errors: %s@]" auth_base e
169169+ in
170170+ match credentials with
171171+ | None -> get client ~sw url out
172172+ | Some _ -> post client ~sw url out
173173+end
174174+175175+type t = {
176176+ display : Display.t;
177177+ client : client;
178178+ cache : Cache.t;
179179+ token : string;
180180+ image : Image.t;
181181+}
182182+183183+let show_blob d =
184184+ match Descriptor.media_type d with
185185+ | OCI
186186+ ( Layer_tar | Layer_tar_gzip | Layer_tar_zstd
187187+ | Layer_non_distributable_tar | Layer_non_distributable_tar_gzip
188188+ | Layer_non_distributable_tar_zstd )
189189+ | Docker (Layer_tar_gzip | Layer_non_distributable_tar_gzip) ->
190190+ true
191191+ | _ -> false
192192+193193+let get_blob ?(show = true) ~sw t d =
194194+ let size = Descriptor.size d in
195195+ let digest = Descriptor.digest d in
196196+ let show = show && show_blob d in
197197+ let () =
198198+ Cache.Blob.if_exists t.cache ~size digest
199199+ ~then_:(fun () ->
200200+ Logs.info (fun l ->
201201+ l "Blob %a is already in the cache" Digest.pp digest);
202202+ ())
203203+ ~else_:(fun () ->
204204+ let bar = Display.line_of_descriptor d in
205205+ Display.with_line ~show ~display:t.display bar (fun r ->
206206+ let progress = Display.report_int r in
207207+ Eio.Switch.run @@ fun sw ->
208208+ let fd =
209209+ API.get_blob ~sw t.client ~progress ~token:t.token t.image d
210210+ in
211211+ Cache.Blob.add_fd t.cache digest fd))
212212+ in
213213+ Cache.Blob.get_fd ~sw t.cache digest
214214+215215+let get_root_manifest ?(show = true) t =
216216+ Cache.Manifest.if_exists t.cache t.image
217217+ ~then_:(fun () ->
218218+ Logs.info (fun l ->
219219+ l "Manifest %a is already in the cache" Image.pp t.image))
220220+ ~else_:(fun () ->
221221+ let line = Display.line_of_image t.image in
222222+ Display.with_line ~show ~display:t.display line @@ fun r ->
223223+ let progress = Display.report_int r in
224224+ let m = API.get_manifest t.client ~progress ~token:t.token t.image in
225225+ Cache.Manifest.add t.cache t.image m);
226226+ Cache.Manifest.get t.cache t.image
227227+228228+let get_manifest ?(show = false) t d =
229229+ let digest = Descriptor.digest d in
230230+ let image = Image.with_digest digest t.image in
231231+ let size = Descriptor.size d in
232232+ let line = Display.line_of_descriptor d in
233233+ Display.with_line ~show ~display:t.display line (fun r ->
234234+ Cache.Manifest.if_exists t.cache image
235235+ ~then_:(fun () ->
236236+ Logs.info (fun l ->
237237+ l "Manifest %a is already in the cache" Digest.pp digest);
238238+ Display.report r size)
239239+ ~else_:(fun () ->
240240+ let progress = Display.report_int r in
241241+ let m = API.get_manifest t.client ~progress ~token:t.token image in
242242+ Cache.Manifest.add t.cache image m));
243243+ Cache.Manifest.get t.cache image
244244+245245+let fetch ?(show_progress = true) ?platform ~cache ~client ~domain_mgr:_
246246+ ?username ?password image =
247247+ Eio.Switch.run @@ fun sw ->
248248+ let display = Display.init ?platform ~sw image in
249249+ let credentials =
250250+ match (username, password) with
251251+ | None, _ -> None
252252+ | Some u, Some p -> Some { API.username = u; password = p }
253253+ | Some u, _ ->
254254+ Fmt.invalid_arg
255255+ "missing credentials for user %s. Use `-p' or set IMAGE_TOKEN." u
256256+ in
257257+ let token = API.get_token client ?credentials image in
258258+ let t = { token; display; cache; client; image } in
259259+ let platform =
260260+ match platform with
261261+ | None -> None
262262+ | Some p -> (
263263+ match Platform.of_string p with
264264+ | Ok p -> Some p
265265+ | Error (`Msg e) -> Fmt.failwith "Fetch.fetch: %s" e)
266266+ in
267267+ let my_platform = platform in
268268+ (* let pool =
269269+ Eio.Executor_pool.create ~sw ~domain_count:4 ~domain_concurrency:25
270270+ domain_mgr
271271+ in
272272+ let get_blob t d =
273273+ Eio.Executor_pool.submit_exn pool (fun () ->
274274+ Eio.Switch.run @@ fun sw -> get_blob ~sw t d)
275275+ in
276276+ *)
277277+ let get_blob t d =
278278+ Eio.Switch.run @@ fun sw -> get_blob ~show:show_progress t ~sw d
279279+ in
280280+ let get_manifest t d = get_manifest ~show:show_progress t d in
281281+ let rec fetch_manifest_descriptor d =
282282+ let platform = Descriptor.platform d in
283283+ let manifest = get_manifest t d in
284284+ fetch_manifest ~platform manifest
285285+ and fetch_layers ~platform config layers =
286286+ match (my_platform, platform) with
287287+ | Some p, Some p' when p <> p' ->
288288+ (* Fmt.epr "XXX SKIP platform=%a\n%!" Platform.pp p'; *)
289289+ ()
290290+ | _ ->
291291+ let _config = get_blob t config in
292292+ let _layers = Eio.Fiber.List.map (get_blob t) layers in
293293+ (* Fmt.epr "XXX CONFIG=%a\n%!" pp config; *)
294294+ (* List.iter (fun l -> Fmt.epr "XXX LAYER=%a\n" pp l) layers) *)
295295+ ()
296296+ and fetch_descriptors ds =
297297+ Logs.info (fun l ->
298298+ let platforms = List.filter_map Descriptor.platform ds in
299299+ l "supported platforms: %a" Fmt.Dump.(list Platform.pp) platforms);
300300+ Eio.Fiber.List.iter fetch_manifest_descriptor ds
301301+ and fetch_manifest ~platform = function
302302+ | `Docker_manifest m ->
303303+ let config = Manifest.Docker.config m in
304304+ let layers = Manifest.Docker.layers m in
305305+ fetch_layers ~platform config layers
306306+ | `Docker_manifest_list m ->
307307+ let ds = Manifest_list.manifests m in
308308+ fetch_descriptors ds
309309+ | `OCI_index i ->
310310+ let ds = Index.manifests i in
311311+ fetch_descriptors ds
312312+ | `OCI_manifest m ->
313313+ let config = Manifest.OCI.config m in
314314+ let layers = Manifest.OCI.layers m in
315315+ fetch_layers ~platform config layers
316316+ in
317317+318318+ let root = get_root_manifest ~show:show_progress t in
319319+ fetch_manifest ~platform root;
320320+ Display.finalise display
···11+open Oci_spec
22+open Astring
33+44+(* TODO: remove code duplication *)
55+let ( let* ) x f = match x with Ok x -> f x | Error e -> Error e
66+let ( let+ ) x f = match x with Ok x -> Ok (f x) | Error e -> Error e
77+let error_msg fmt = Fmt.kstr (fun s -> Error (`Msg s)) fmt
88+99+type t = {
1010+ org : string option;
1111+ name : string;
1212+ tag : string option;
1313+ digest : Digest.t option;
1414+}
1515+1616+let digest t = t.digest
1717+let tag t = t.tag
1818+let org t = match t.org with None -> "library" | Some o -> o
1919+let name t = t.name
2020+let repository t = org t ^ "/" ^ t.name
2121+2222+let reference t =
2323+ match (t.tag, t.digest) with
2424+ | Some t, None -> t
2525+ | _, Some t -> Digest.to_string t
2626+ | None, None -> assert false
2727+2828+let of_string str =
2929+ let* str, digest =
3030+ match String.cut ~sep:"@" str with
3131+ | None -> Ok (str, None)
3232+ | Some (path, digest) ->
3333+ let+ digest = Digest.of_string digest in
3434+ (path, Some digest)
3535+ in
3636+ let str, tag =
3737+ match String.cut ~sep:":" str with
3838+ | None -> (str, None)
3939+ | Some (p, o) -> (p, Some o)
4040+ in
4141+ let org, name =
4242+ match String.cut ~sep:"/" str with
4343+ | None -> (None, str)
4444+ | Some (p, i) -> (Some p, i)
4545+ in
4646+ if name = "sha265" then error_msg "missing image name"
4747+ else
4848+ let tag =
4949+ match (tag, digest) with None, None -> Some "latest" | _ -> tag
5050+ in
5151+ Ok { org; name; tag; digest }
5252+5353+let v ?digest ?tag n =
5454+ match of_string n with
5555+ | Ok image -> { image with digest; tag }
5656+ | Error (`Msg e) -> Fmt.invalid_arg "Image.v(%s): error %s" n e
5757+5858+let pp ppf t =
5959+ let pp_org ppf = function
6060+ | None | Some "library" -> ()
6161+ | Some s -> Fmt.pf ppf "%s/" s
6262+ in
6363+ let pp_tag ppf = function None -> () | Some s -> Fmt.pf ppf ":%s" s in
6464+ let pp_digest ppf = function
6565+ | None -> ()
6666+ | Some s -> Fmt.pf ppf "@%a" Digest.pp s
6767+ in
6868+ Fmt.pf ppf "%a%s%a%a" pp_org t.org t.name pp_tag t.tag pp_digest t.digest
6969+7070+let to_string = Fmt.to_to_string pp
7171+let with_tag tag t = { t with tag = Some tag }
7272+let with_digest d t = { t with digest = Some d }
+21
src/image.mli
···11+open Oci_spec
22+33+type t
44+55+val v : ?digest:Digest.t -> ?tag:string -> string -> t
66+(** [v repository] *)
77+88+val pp : t Fmt.t
99+val to_string : t -> string
1010+val of_string : string -> (t, [ `Msg of string ]) result
1111+val reference : t -> string
1212+val org : t -> string
1313+val name : t -> string
1414+1515+val repository : t -> string
1616+(** [repository t] is [org t ^ "/" ^ name t] *)
1717+1818+val digest : t -> Digest.t option
1919+val tag : t -> string option
2020+val with_tag : string -> t -> t
2121+val with_digest : Digest.t -> t -> t
+77
src/ls.ml
···11+open Oci_spec
22+open Optint
33+44+type t = {
55+ repository : string;
66+ tags : string list;
77+ digest : Digest.t;
88+ platform : Platform.t option;
99+ size : string;
1010+}
1111+1212+let repository t = t.repository
1313+let tags t = t.tags
1414+let digest t = t.digest
1515+let platform t = t.platform
1616+let size t = t.size
1717+let sizes = [| "B"; "KiB"; "MiB"; "GiB"; "TiB"; "PiB"; "EiB"; "ZiB"; "YiB" |]
1818+1919+let bytes_to_size ?(decimals = 2) n =
2020+ if n = Int63.zero then Fmt.str "0 byte"
2121+ else
2222+ let n = Int63.to_float n in
2323+ let i = Float.floor (Float.log n /. Float.log 1024.) in
2424+ let r = n /. Float.pow 1024. i in
2525+ Fmt.str "%.*f %s" decimals r sizes.(int_of_float i)
2626+2727+let of_image ~tags ~cache i =
2828+ let repository =
2929+ match Image.org i with "library" -> Image.name i | _ -> Image.repository i
3030+ in
3131+ let digest =
3232+ match Image.digest i with Some d -> d | None -> assert false
3333+ (* it's always a Some x because we used Cache.Manifest.list_digest
3434+ -- would be nice to enforce this statically *)
3535+ in
3636+ let tags = Hashtbl.find_all tags digest in
3737+ let m = Cache.Manifest.get cache i in
3838+ let read d =
3939+ let k = Descriptor.digest d in
4040+ let media_type = Descriptor.media_type d in
4141+ let b = Cache.Blob.get_string cache k in
4242+ match Config.of_string ~media_type b with
4343+ | Ok c -> c
4444+ | Error (`Msg e) ->
4545+ Fmt.failwith "TODO: %a %a %s" Digest.pp k Media_type.pp media_type e
4646+ in
4747+ let platform = Manifest.platform read m in
4848+ let size =
4949+ match Manifest.size m with Some s -> bytes_to_size s | None -> "-"
5050+ in
5151+ { repository; tags; digest; platform; size }
5252+5353+let list ~cache =
5454+ let all_tags = Cache.Manifest.list_tags cache in
5555+ let tags = Hashtbl.create (List.length all_tags) in
5656+ List.iter
5757+ (fun i ->
5858+ let tag =
5959+ match Image.tag i with Some t -> t | None -> assert false
6060+ (* it's always a Some x because we use
6161+ Cache.Manfifest.list_tags. It would be nice to enfore it
6262+ statically. *)
6363+ in
6464+ let m = Cache.Manifest.get cache i in
6565+ let ms = Manifest.manifests m in
6666+ List.iter
6767+ (fun d ->
6868+ let digest = Descriptor.digest d in
6969+ Hashtbl.add tags digest tag)
7070+ ms)
7171+ all_tags;
7272+7373+ let is = Cache.Manifest.list_digests cache in
7474+ let ts = List.map (of_image ~tags ~cache) is in
7575+ (* FIXME: should use Descriptor.attestation_manifest on the index
7676+ instead *)
7777+ List.filter (fun t -> t.platform <> Some Platform.unknown) ts
+10
src/ls.mli
···11+open Oci_spec
22+33+type t
44+55+val list : cache:Cache.t -> t list
66+val repository : t -> string
77+val tags : t -> string list
88+val digest : t -> Digest.t
99+val platform : t -> Platform.t option
1010+val size : t -> string
+11
src/oci.ml
···11+module Spec = Oci_spec
22+module Cache = Cache
33+module Image = Image
44+module List = Ls
55+module Util = Util
66+module Fetch = Fetch
77+88+let fetch = Fetch.fetch
99+let list = List.list
1010+let checkout = Checkout.checkout
1111+let show = Show.show
···11+open Oci_spec
22+33+let show ~cache i =
44+ Fmt.pr "🔎 Showing %a\n%!" Fmt.(styled `Cyan Image.pp) i;
55+ let m = Cache.Manifest.get cache i in
66+ let pp = Oci_spec.Common.pp_json in
77+ match m with
88+ | `Docker_manifest m ->
99+ Fmt.pr "DOCKER MANIFEST:\n%a\n%!" pp (Manifest.Docker.to_yojson m)
1010+ | `OCI_manifest m ->
1111+ Fmt.pr "OCI MANIFEST:\n%a\n%!" pp (Manifest.OCI.to_yojson m)
1212+ | `OCI_index i -> Fmt.pr "OCI INDEX:\n%a\n%!" pp (Index.to_yojson i)
1313+ | `Docker_manifest_list l ->
1414+ Fmt.pr "DOCKER MANIFEST LIST:\n%a\n%!" pp (Manifest_list.to_yojson l)
+63
src/spec/OS.ml
···11+open Common
22+33+type t =
44+ | Aix
55+ | Android
66+ | Darwin
77+ | Dragonfly
88+ | Freebsd
99+ | Illumos
1010+ | Ios
1111+ | Js
1212+ | Linux
1313+ | Netbsd
1414+ | Openbsd
1515+ | Plan9
1616+ | Solaris
1717+ | Wasip1
1818+ | Windows
1919+ | Unknown
2020+2121+let to_string = function
2222+ | Aix -> "aix"
2323+ | Android -> "android"
2424+ | Darwin -> "darwin"
2525+ | Dragonfly -> "dragonfly"
2626+ | Freebsd -> "freebsd"
2727+ | Illumos -> "illumos"
2828+ | Ios -> "ios"
2929+ | Js -> "js"
3030+ | Linux -> "linux"
3131+ | Netbsd -> "netbsd"
3232+ | Openbsd -> "openbsd"
3333+ | Plan9 -> "plan9"
3434+ | Solaris -> "solaris"
3535+ | Wasip1 -> "wasip1"
3636+ | Windows -> "windows"
3737+ | Unknown -> "unknown"
3838+3939+let of_string = function
4040+ | "aix" -> Ok Aix
4141+ | "android" -> Ok Android
4242+ | "darwin" -> Ok Darwin
4343+ | "dragonfly" -> Ok Dragonfly
4444+ | "freebsd" -> Ok Freebsd
4545+ | "illumos" -> Ok Illumos
4646+ | "ios" -> Ok Ios
4747+ | "js" -> Ok Js
4848+ | "linux" -> Ok Linux
4949+ | "netbsd" -> Ok Netbsd
5050+ | "openbsd" -> Ok Openbsd
5151+ | "plan9" -> Ok Plan9
5252+ | "solaris" -> Ok Solaris
5353+ | "wasip1" -> Ok Wasip1
5454+ | "windows" -> Ok Windows
5555+ | "unknown" -> Ok Unknown
5656+ | s -> error_msg "OS.of_string: invalid string (%S)" s
5757+5858+let jsont =
5959+ Jsont.map ~kind:"os"
6060+ ~dec:(fun s -> match of_string s with Ok v -> v | Error _ -> Unknown)
6161+ ~enc:to_string Jsont.string
6262+6363+let pp = Fmt.of_to_string to_string
+25
src/spec/OS.mli
···11+(** This property specifies the operating system. Image indexes SHOULD use, and
22+ implementations SHOULD understand, values listed in the Go Language document
33+ for GOOS. *)
44+type t =
55+ | Aix
66+ | Android
77+ | Darwin
88+ | Dragonfly
99+ | Freebsd
1010+ | Illumos
1111+ | Ios
1212+ | Js
1313+ | Linux
1414+ | Netbsd
1515+ | Openbsd
1616+ | Plan9
1717+ | Solaris
1818+ | Wasip1
1919+ | Windows
2020+ | Unknown
2121+2222+val jsont : t Jsont.t
2323+val pp : t Fmt.t
2424+val of_string : string -> (t, [ `Msg of string ]) result
2525+val to_string : t -> string
···11+(** Variant type representing different kinds of OCI image annotations. *)
22+type t =
33+ | Created
44+ | Authors
55+ | Url
66+ | Documentation
77+ | Source
88+ | Version
99+ | Revision
1010+ | Vendor
1111+ | Licenses
1212+ | Ref_name
1313+ | Title
1414+ | Description
1515+ | Base_image_digest
1616+ | Base_image_name
1717+ | Reference_digest
1818+ | Reference_type
1919+ | Other of string
2020+2121+val jsont : t Jsont.t
2222+2323+val to_string : t -> string
2424+(** [to_string a] converts an annotation variant to its corresponding string. *)
2525+2626+val of_string : string -> t
2727+(** [of_string s] tries to convert a string [s] to its corresponding annotation
2828+ variant. Returns [None] if the string does not match any known annotation.
2929+*)
+82
src/spec/arch.ml
···11+open Common
22+33+type t =
44+ | X386
55+ | Xamd64
66+ | Arm
77+ | Arm64
88+ | Wasm
99+ | Loong64
1010+ | Mips
1111+ | Mipsle
1212+ | Mips64
1313+ | Mips64le
1414+ | Ppc64
1515+ | Ppc64le
1616+ | Riscv64
1717+ | S390x
1818+ | Unknown
1919+2020+let to_string = function
2121+ | X386 -> "386"
2222+ | Xamd64 -> "amd64"
2323+ | Arm -> "arm"
2424+ | Arm64 -> "arm64"
2525+ | Wasm -> "wasm"
2626+ | Loong64 -> "loong64"
2727+ | Mips -> "mips"
2828+ | Mipsle -> "mipsle"
2929+ | Mips64 -> "mips64"
3030+ | Mips64le -> "mips64le"
3131+ | Ppc64 -> "ppc64"
3232+ | Ppc64le -> "ppc64le"
3333+ | Riscv64 -> "riscv64"
3434+ | S390x -> "s390x"
3535+ | Unknown -> "unknown"
3636+3737+let of_string = function
3838+ | "386" -> Ok X386
3939+ | "amd64" -> Ok Xamd64
4040+ | "arm" -> Ok Arm
4141+ | "arm64" -> Ok Arm64
4242+ | "wasm" -> Ok Wasm
4343+ | "loong64" -> Ok Loong64
4444+ | "mips" -> Ok Mips
4545+ | "mipsle" -> Ok Mipsle
4646+ | "mips64" -> Ok Mips64
4747+ | "mips64le" -> Ok Mips64le
4848+ | "ppc64" -> Ok Ppc64
4949+ | "ppc64le" -> Ok Ppc64le
5050+ | "riscv64" -> Ok Riscv64
5151+ | "s390x" -> Ok S390x
5252+ | "unknown" -> Ok Unknown
5353+ | s -> error_msg "Arch.of_string: invalid string (%S)" s
5454+5555+let jsont =
5656+ Jsont.map ~kind:"arch"
5757+ ~dec:(fun s -> match of_string s with Ok v -> v | Error _ -> Unknown)
5858+ ~enc:to_string Jsont.string
5959+6060+let pp = Fmt.of_to_string to_string
6161+6262+type variant = V5 | V6 | V7 | V8
6363+6464+let variant_to_string = function
6565+ | V5 -> "v5"
6666+ | V6 -> "v6"
6767+ | V7 -> "v7"
6868+ | V8 -> "v8"
6969+7070+let variant_of_string = function
7171+ | "v5" -> Ok V5
7272+ | "v6" -> Ok V6
7373+ | "v7" -> Ok V7
7474+ | "v8" -> Ok V8
7575+ | s -> error_msg "Arch.variant_of_string: invalid string (%S)" s
7676+7777+let variant_jsont =
7878+ Jsont.map ~kind:"arch.variant"
7979+ ~dec:(fun s -> match variant_of_string s with Ok v -> v | Error _ -> V8)
8080+ ~enc:variant_to_string Jsont.string
8181+8282+let pp_variant = Fmt.of_to_string variant_to_string
+30
src/spec/arch.mli
···11+(** This property specifies the CPU architecture. Image indexes SHOULD use, and
22+ implementations SHOULD understand, values listed in the Go Language document
33+ for GOARCH. *)
44+type t =
55+ | X386
66+ | Xamd64
77+ | Arm
88+ | Arm64
99+ | Wasm
1010+ | Loong64
1111+ | Mips
1212+ | Mipsle
1313+ | Mips64
1414+ | Mips64le
1515+ | Ppc64
1616+ | Ppc64le
1717+ | Riscv64
1818+ | S390x
1919+ | Unknown
2020+2121+val jsont : t Jsont.t
2222+val pp : t Fmt.t
2323+val of_string : string -> (t, [ `Msg of string ]) result
2424+val to_string : t -> string
2525+2626+type variant = V5 | V6 | V7 | V8
2727+2828+val variant_jsont : variant Jsont.t
2929+val variant_of_string : string -> (variant, [ `Msg of string ]) result
3030+val pp_variant : variant Fmt.t
+15
src/spec/auth.ml
···11+open Common
22+33+type t = { access_token : string }
44+55+let jsont =
66+ Jsont.Object.map ~kind:"auth" (fun access_token -> { access_token })
77+ |> Jsont.Object.mem "access_token" Jsont.string ~enc:(fun t -> t.access_token)
88+ |> Jsont.Object.skip_unknown |> Jsont.Object.finish
99+1010+let of_string s =
1111+ match Jsont_bytesrw.decode_string jsont s with
1212+ | Ok t -> Ok t
1313+ | Error e -> error_msg "Auth.of_string: %s" e
1414+1515+let token t = t.access_token
+7
src/spec/auth.mli
···11+type t
22+33+val jsont : t Jsont.t
44+val of_string : string -> (t, [ `Msg of string ]) result
55+val token : t -> string
66+77+(* TODO: implement JWT spec: https://distribution.github.io/distribution/spec/auth/jwt/ *)
+151
src/spec/blob.ml
···11+open Common
22+33+module OCI = struct
44+ type t =
55+ | Empty
66+ | Descriptor of Descriptor.t
77+ | Layout_header of Layout.t
88+ | Image_index of Index.t
99+ | Image_manifest of Manifest.OCI.t
1010+ | Image_config of Config.OCI.t
1111+ | Raw of string
1212+1313+ let pp ppf = function
1414+ | Empty -> Fmt.string ppf ""
1515+ | Descriptor d -> pp_json ppf (Descriptor.to_yojson d)
1616+ | Layout_header h -> pp_json ppf (Layout.to_yojson h)
1717+ | Image_index i -> pp_json ppf (Index.to_yojson i)
1818+ | Image_manifest m -> pp_json ppf (Manifest.OCI.to_yojson m)
1919+ | Image_config c -> pp_json ppf (Config.OCI.to_yojson c)
2020+ | Raw _ -> Fmt.string ppf "<raw>"
2121+2222+ let descriptor str =
2323+ let* json = json_of_string str in
2424+ let+ d = Descriptor.of_yojson json in
2525+ Descriptor d
2626+2727+ let layout_header str =
2828+ let* json = json_of_string str in
2929+ let+ l = Layout.of_yojson json in
3030+ Layout_header l
3131+3232+ let image_index str =
3333+ let* json = json_of_string str in
3434+ let+ i = Index.of_yojson json in
3535+ Image_index i
3636+3737+ let image_manifest str =
3838+ let* json = json_of_string str in
3939+ let+ m = Manifest.OCI.of_yojson json in
4040+ Image_manifest m
4141+4242+ let image_config str =
4343+ let* json = json_of_string str in
4444+ let+ c = Config.OCI.of_yojson json in
4545+ Image_config c
4646+4747+ let trust str = Ok (Raw str) (* TODO *)
4848+ let layer str = Ok (Raw str) (* TODO *)
4949+5050+ let of_string ty str =
5151+ wrap
5252+ @@
5353+ match ty with
5454+ | Media_type.OCI.Empty -> Ok Empty
5555+ | Descriptor -> descriptor str
5656+ | Layout_header -> layout_header str
5757+ | Image_index -> image_index str
5858+ | Image_manifest -> image_manifest str
5959+ | Image_config -> image_config str
6060+ | Layer_tar -> layer str
6161+ | Layer_tar_gzip -> layer str
6262+ | Layer_tar_zstd -> layer str
6363+ | Layer_non_distributable_tar -> layer str
6464+ | Layer_non_distributable_tar_gzip -> layer str
6565+ | Layer_non_distributable_tar_zstd -> layer str
6666+ | Trust -> trust str
6767+ | Other _ -> Ok (Raw str)
6868+end
6969+7070+module Docker = struct
7171+ type t =
7272+ | Image_manifest of Manifest.Docker.t
7373+ | Image_manifest_list of Manifest_list.t
7474+ | Image_config of Config.Docker.t
7575+ | Plugin_config of Jsont.json
7676+ | Raw of string
7777+7878+ let pp ppf = function
7979+ | Image_manifest m -> pp_json ppf (Manifest.Docker.to_yojson m)
8080+ | Image_manifest_list l -> pp_json ppf (Manifest_list.to_yojson l)
8181+ | Image_config c -> pp_json ppf (Config.Docker.to_yojson c)
8282+ | Plugin_config j -> pp_json ppf j
8383+ | Raw _ -> Fmt.string ppf "<raw>"
8484+8585+ let image_manifest str =
8686+ let* json = json_of_string str in
8787+ let+ m = Manifest.Docker.of_yojson json in
8888+ Image_manifest m
8989+9090+ let image_manifest_list str =
9191+ let* json = json_of_string str in
9292+ let+ m = Manifest_list.of_yojson json in
9393+ Image_manifest_list m
9494+9595+ let image_config str =
9696+ let* json = json_of_string str in
9797+ let+ c = Config.Docker.of_yojson json in
9898+ Image_config c
9999+100100+ let plugin str =
101101+ let+ json = json_of_string str in
102102+ Plugin_config json
103103+104104+ let layer str = Ok (Raw str) (* TODO *)
105105+106106+ let of_string ty str =
107107+ wrap
108108+ @@
109109+ match ty with
110110+ | Media_type.Docker.Image_manifest -> image_manifest str
111111+ | Image_manifest_list -> image_manifest_list str
112112+ | Image_config -> image_config str
113113+ | Plugin_config -> plugin str
114114+ | Layer_tar_gzip -> layer str
115115+ | Layer_non_distributable_tar_gzip -> layer str
116116+end
117117+118118+type v = OCI of OCI.t | Docker of Docker.t
119119+type t = { media_type : Media_type.t; v : v }
120120+121121+let v t = t.v
122122+123123+let pp ppf t =
124124+ match t.v with OCI t -> OCI.pp ppf t | Docker t -> Docker.pp ppf t
125125+126126+let of_string ~media_type str =
127127+ let+ v =
128128+ match media_type with
129129+ | Media_type.OCI m ->
130130+ let+ t = OCI.of_string m str in
131131+ OCI t
132132+ | Docker m ->
133133+ let+ t = Docker.of_string m str in
134134+ Docker t
135135+ in
136136+ { media_type; v }
137137+138138+let media_type t = t.media_type
139139+140140+let err_size e g =
141141+ error_msg "Blob.of_descriptor: invalid size: expected %a, got %d" Int63.pp e g
142142+143143+let of_descriptor d body =
144144+ let digest = Descriptor.digest d in
145145+ let expected_size = Descriptor.size d in
146146+ let got_size = String.length body in
147147+ if Int63.of_int got_size <> expected_size then err_size expected_size got_size
148148+ else
149149+ let* () = Digest.validate digest body in
150150+ let media_type = Descriptor.media_type d in
151151+ of_string ~media_type body
+31
src/spec/blob.mli
···11+module OCI : sig
22+ type t =
33+ | Empty
44+ | Descriptor of Descriptor.t
55+ | Layout_header of Layout.t
66+ | Image_index of Index.t
77+ | Image_manifest of Manifest.OCI.t
88+ | Image_config of Config.OCI.t
99+ | Raw of string
1010+end
1111+1212+module Docker : sig
1313+ type t =
1414+ | Image_manifest of Manifest.Docker.t
1515+ | Image_manifest_list of Manifest_list.t
1616+ | Image_config of Config.Docker.t
1717+ | Plugin_config of Jsont.json
1818+ | Raw of string
1919+end
2020+2121+type v = OCI of OCI.t | Docker of Docker.t
2222+type t
2323+2424+val pp : t Fmt.t
2525+2626+val of_string :
2727+ media_type:Media_type.t -> string -> (t, [ `Msg of string ]) result
2828+2929+val media_type : t -> Media_type.t
3030+val v : t -> v
3131+val of_descriptor : Descriptor.t -> string -> (t, [ `Msg of string ]) result
+174
src/spec/common.ml
···11+open Astring
22+33+let error fmt = Fmt.kstr (fun s -> Error s) fmt
44+let error_msg fmt = Fmt.kstr (fun s -> Error (`Msg s)) fmt
55+66+let pp_json ppf t =
77+ match Jsont_bytesrw.encode_string Jsont.json t with
88+ | Ok s -> Fmt.string ppf s
99+ | Error e -> Fmt.pf ppf "<error: %s>" e
1010+1111+let unwrap = function Ok _ as ok -> ok | Error (`Msg e) -> Error e
1212+let wrap = function Ok _ as ok -> ok | Error e -> Error (`Msg e)
1313+let ( let* ) x f = match x with Ok x -> f x | Error e -> Error e
1414+let ( let+ ) x f = match x with Ok x -> Ok (f x) | Error e -> Error e
1515+1616+(** {1 Jsont helpers} *)
1717+1818+let json_of_string str =
1919+ match Jsont_bytesrw.decode_string Jsont.json str with
2020+ | Ok json -> Ok json
2121+ | Error e -> Error e
2222+2323+let json_to_string json =
2424+ match Jsont_bytesrw.encode_string Jsont.json json with
2525+ | Ok s -> s
2626+ | Error _ -> "{}"
2727+2828+(** {1 Date/time} *)
2929+3030+type date_time = Ptime.t * Ptime.tz_offset_s option
3131+3232+let date_time_jsont =
3333+ Jsont.map ~kind:"date_time"
3434+ ~dec:(fun s ->
3535+ match Ptime.rfc3339_string_error (Ptime.of_rfc3339 s) with
3636+ | Ok (t, tz, _) -> (t, tz)
3737+ | Error _ -> (Ptime.epoch, None))
3838+ ~enc:(fun (t, tz) -> Ptime.to_rfc3339 ?tz_offset_s:tz t)
3939+ Jsont.string
4040+4141+(** {1 Map type} *)
4242+4343+type ('a, 'b) map = ('a * 'b) list
4444+4545+(* Helper to convert between JSON object and association list.
4646+ Uses Jsont.Object.Mems.string_map for proper type handling. *)
4747+let map_jsont key_of_string key_to_string value_jsont =
4848+ let mems = Jsont.Object.Mems.string_map value_jsont in
4949+ Jsont.Object.map ~kind:"map" Fun.id
5050+ |> Jsont.Object.keep_unknown ~enc:Fun.id mems
5151+ |> Jsont.Object.finish
5252+ |> Jsont.map ~kind:"map_list"
5353+ ~dec:(fun m ->
5454+ (* The type is 'a Map.Make(String).t - we use Obj.magic to treat it as a fold-able structure *)
5555+ (* This is safe because Map.Make(String) is structurally the same regardless of module application *)
5656+ let module M = Map.Make (String) in
5757+ (Obj.magic M.fold
5858+ : (string -> 'a -> 'b list -> 'b list) -> 'c -> 'b list -> 'b list)
5959+ (fun k v acc ->
6060+ match key_of_string k with
6161+ | Ok key -> (key, v) :: acc
6262+ | Error _ -> acc)
6363+ m [])
6464+ ~enc:(fun alist ->
6565+ let module M = Map.Make (String) in
6666+ let m =
6767+ List.fold_left
6868+ (fun m (k, v) -> M.add (key_to_string k) v m)
6969+ M.empty alist
7070+ in
7171+ (* Same Obj.magic trick in reverse *)
7272+ (Obj.magic m : 'c))
7373+7474+(** {1 Nil type} *)
7575+7676+type nil = Nil
7777+7878+let nil_jsont =
7979+ Jsont.Object.map ~kind:"nil" Nil
8080+ |> Jsont.Object.skip_unknown |> Jsont.Object.finish
8181+8282+(** {1 Environment variable} *)
8383+8484+type env = string * string
8585+8686+let env_jsont =
8787+ Jsont.map ~kind:"env"
8888+ ~dec:(fun s ->
8989+ match String.cut ~sep:"=" s with
9090+ | Some (k, v) -> (k, v)
9191+ | None ->
9292+ Jsont.Error.msgf Jsont.Meta.none
9393+ "Env value invalid, must be in the format KEY=VALUE")
9494+ ~enc:(fun (k, v) -> k ^ "=" ^ v)
9595+ Jsont.string
9696+9797+(** {1 Set (object with empty values)} *)
9898+9999+type set = string list
100100+101101+let set_jsont =
102102+ let mems = Jsont.Object.Mems.string_map nil_jsont in
103103+ Jsont.Object.map ~kind:"set" Fun.id
104104+ |> Jsont.Object.keep_unknown ~enc:Fun.id mems
105105+ |> Jsont.Object.finish
106106+ |> Jsont.map ~kind:"set_list"
107107+ ~dec:(fun m ->
108108+ let module M = Map.Make (String) in
109109+ (Obj.magic M.fold
110110+ : (string -> nil -> string list -> string list) ->
111111+ 'c ->
112112+ string list ->
113113+ string list)
114114+ (fun k _ acc -> k :: acc)
115115+ m [])
116116+ ~enc:(fun lst ->
117117+ let module M = Map.Make (String) in
118118+ let m = List.fold_left (fun m k -> M.add k Nil m) M.empty lst in
119119+ (Obj.magic m : 'c))
120120+121121+(** {1 Schema version} *)
122122+123123+type v2 = V2
124124+125125+let v2_jsont =
126126+ Jsont.map ~kind:"v2"
127127+ ~dec:(fun n -> if n = 2 then V2 else failwith "expecting schemaVersion 2")
128128+ ~enc:(fun V2 -> 2)
129129+ Jsont.int
130130+131131+(** {1 RFC 6838 media type} *)
132132+133133+type rfc_6838 = string
134134+135135+let rfc_6838_jsont = Jsont.string
136136+137137+(** {1 Int63 (large integers)} *)
138138+139139+open Optint
140140+141141+type z = Int63.t
142142+143143+(* Strict integer decoder that rejects strings per OCI spec *)
144144+let z_jsont =
145145+ Jsont.any ~kind:"int63"
146146+ ~dec_number:
147147+ (Jsont.map ~kind:"int63_number" ~dec:Int63.of_int64 ~enc:Int63.to_int64
148148+ Jsont.int64)
149149+ ~enc:(fun _ ->
150150+ Jsont.map ~kind:"int63_enc" ~dec:Int63.of_int64 ~enc:Int63.to_int64
151151+ Jsont.int64)
152152+ ()
153153+154154+module Int63 = Int63
155155+156156+(** {1 Base64 encoded strings} *)
157157+158158+module Base64 = struct
159159+ type t = string
160160+161161+ let of_raw x = x
162162+163163+ let jsont =
164164+ Jsont.map ~kind:"base64"
165165+ ~dec:(fun s ->
166166+ match Base64.decode s with
167167+ | Ok _ -> s
168168+ | Error (`Msg e) ->
169169+ Jsont.Error.msgf Jsont.Meta.none "invalid base64: %s" e)
170170+ ~enc:Fun.id Jsont.string
171171+172172+ let decode u = Base64.decode u
173173+ let encode u = Base64.encode_string u
174174+end
+63
src/spec/common.mli
···11+module Int63 = Optint.Int63
22+33+(** Basic types *)
44+55+type date_time = Ptime.t * Ptime.tz_offset_s option
66+type ('a, 'b) map = ('a * 'b) list
77+type env = string * string
88+type set = string list
99+type nil = Nil
1010+type v2 = V2
1111+type rfc_6838 = string
1212+type z = Int63.t
1313+1414+(** Jsont codecs *)
1515+1616+val date_time_jsont : date_time Jsont.t
1717+1818+val map_jsont :
1919+ (string -> ('a, string) result) ->
2020+ ('a -> string) ->
2121+ 'b Jsont.t ->
2222+ ('a, 'b) map Jsont.t
2323+2424+val env_jsont : env Jsont.t
2525+val set_jsont : set Jsont.t
2626+val nil_jsont : nil Jsont.t
2727+val v2_jsont : v2 Jsont.t
2828+val rfc_6838_jsont : rfc_6838 Jsont.t
2929+val z_jsont : z Jsont.t
3030+3131+(** Result *)
3232+3333+val ( let* ) : ('a, 'b) result -> ('a -> ('c, 'b) result) -> ('c, 'b) result
3434+val ( let+ ) : ('a, 'b) result -> ('a -> 'c) -> ('c, 'b) result
3535+3636+(** Errors *)
3737+3838+val error : ('a, Format.formatter, unit, ('b, string) result) format4 -> 'a
3939+4040+val error_msg :
4141+ ('a, Format.formatter, unit, ('b, [ `Msg of string ]) result) format4 -> 'a
4242+4343+val unwrap : ('a, [ `Msg of string ]) result -> ('a, string) result
4444+val wrap : ('a, string) result -> ('a, [ `Msg of string ]) result
4545+4646+(** JSON *)
4747+4848+val pp_json : Jsont.json Fmt.t
4949+val json_of_string : string -> (Jsont.json, string) result
5050+5151+val json_to_string : Jsont.json -> string
5252+(** Returns "{}" if encoding fails *)
5353+5454+(** Base64 *)
5555+5656+module Base64 : sig
5757+ type t
5858+5959+ val jsont : t Jsont.t
6060+ val of_raw : string -> t
6161+ val decode : t -> (string, [ `Msg of string ]) result
6262+ val encode : string -> t
6363+end
+519
src/spec/config.ml
···11+open Common
22+33+let labels_jsont =
44+ map_jsont
55+ (fun s -> Ok (Annotation.of_string s))
66+ Annotation.to_string Jsont.string
77+88+module OCI = struct
99+ type config = {
1010+ user : string option;
1111+ exposed_ports : set;
1212+ env : env list;
1313+ entrypoint : string list;
1414+ cmd : string list;
1515+ volumes : set;
1616+ working_dir : string option;
1717+ labels : (Annotation.t, string) map;
1818+ stop_signal : string option;
1919+ args_escaped : bool option;
2020+ memory : int option;
2121+ memory_swap : int option;
2222+ cpu_shares : int option;
2323+ healthcheck : set;
2424+ on_build : string option;
2525+ }
2626+2727+ let config_jsont =
2828+ Jsont.Object.map ~kind:"oci_config"
2929+ (fun
3030+ user
3131+ exposed_ports
3232+ env
3333+ entrypoint
3434+ cmd
3535+ volumes
3636+ working_dir
3737+ labels
3838+ stop_signal
3939+ args_escaped
4040+ memory
4141+ memory_swap
4242+ cpu_shares
4343+ healthcheck
4444+ on_build
4545+ ->
4646+ {
4747+ user;
4848+ exposed_ports = Option.value ~default:[] exposed_ports;
4949+ env = Option.value ~default:[] env;
5050+ entrypoint = Option.value ~default:[] entrypoint;
5151+ cmd = Option.value ~default:[] cmd;
5252+ volumes = Option.value ~default:[] volumes;
5353+ working_dir;
5454+ labels = Option.value ~default:[] labels;
5555+ stop_signal;
5656+ args_escaped;
5757+ memory;
5858+ memory_swap;
5959+ cpu_shares;
6060+ healthcheck = Option.value ~default:[] healthcheck;
6161+ on_build;
6262+ })
6363+ |> Jsont.Object.opt_mem "User" Jsont.string ~enc:(fun c -> c.user)
6464+ |> Jsont.Object.opt_mem "ExposedPorts" set_jsont ~enc:(fun c ->
6565+ if c.exposed_ports = [] then None else Some c.exposed_ports)
6666+ |> Jsont.Object.opt_mem "Env" (Jsont.list env_jsont) ~enc:(fun c ->
6767+ if c.env = [] then None else Some c.env)
6868+ |> Jsont.Object.opt_mem "Entrypoint" (Jsont.list Jsont.string)
6969+ ~enc:(fun c -> if c.entrypoint = [] then None else Some c.entrypoint)
7070+ |> Jsont.Object.opt_mem "Cmd" (Jsont.list Jsont.string) ~enc:(fun c ->
7171+ if c.cmd = [] then None else Some c.cmd)
7272+ |> Jsont.Object.opt_mem "Volumes" set_jsont ~enc:(fun c ->
7373+ if c.volumes = [] then None else Some c.volumes)
7474+ |> Jsont.Object.opt_mem "WorkingDir" Jsont.string ~enc:(fun c ->
7575+ c.working_dir)
7676+ |> Jsont.Object.opt_mem "Labels" labels_jsont ~enc:(fun c ->
7777+ if c.labels = [] then None else Some c.labels)
7878+ |> Jsont.Object.opt_mem "StopSignal" Jsont.string ~enc:(fun c ->
7979+ c.stop_signal)
8080+ |> Jsont.Object.opt_mem "ArgsEscaped" Jsont.bool ~enc:(fun c ->
8181+ c.args_escaped)
8282+ |> Jsont.Object.opt_mem "Memory" Jsont.int ~enc:(fun c -> c.memory)
8383+ |> Jsont.Object.opt_mem "MemorySwap" Jsont.int ~enc:(fun c -> c.memory_swap)
8484+ |> Jsont.Object.opt_mem "CpuShares" Jsont.int ~enc:(fun c -> c.cpu_shares)
8585+ |> Jsont.Object.opt_mem "HealthCheck" set_jsont ~enc:(fun c ->
8686+ if c.healthcheck = [] then None else Some c.healthcheck)
8787+ |> Jsont.Object.opt_mem "OnBuild" Jsont.string ~enc:(fun c -> c.on_build)
8888+ |> Jsont.Object.skip_unknown |> Jsont.Object.finish
8989+9090+ type rootfs = { type_ : string; diff_ids : Digest.t list }
9191+9292+ let rootfs_jsont =
9393+ Jsont.Object.map ~kind:"rootfs" (fun type_ diff_ids -> { type_; diff_ids })
9494+ |> Jsont.Object.mem "type" Jsont.string ~enc:(fun r -> r.type_)
9595+ |> Jsont.Object.mem "diff_ids" (Jsont.list Digest.jsont) ~enc:(fun r ->
9696+ r.diff_ids)
9797+ |> Jsont.Object.finish
9898+9999+ type history = {
100100+ created : date_time option;
101101+ author : string option;
102102+ created_by : string option;
103103+ comment : string option;
104104+ empty_layer : bool;
105105+ }
106106+107107+ let history_jsont =
108108+ Jsont.Object.map ~kind:"history"
109109+ (fun created author created_by comment empty_layer ->
110110+ {
111111+ created;
112112+ author;
113113+ created_by;
114114+ comment;
115115+ empty_layer = Option.value ~default:false empty_layer;
116116+ })
117117+ |> Jsont.Object.opt_mem "created" date_time_jsont ~enc:(fun h -> h.created)
118118+ |> Jsont.Object.opt_mem "author" Jsont.string ~enc:(fun h -> h.author)
119119+ |> Jsont.Object.opt_mem "created_by" Jsont.string ~enc:(fun h ->
120120+ h.created_by)
121121+ |> Jsont.Object.opt_mem "comment" Jsont.string ~enc:(fun h -> h.comment)
122122+ |> Jsont.Object.opt_mem "empty_layer" Jsont.bool ~enc:(fun h ->
123123+ if h.empty_layer then Some true else None)
124124+ |> Jsont.Object.skip_unknown |> Jsont.Object.finish
125125+126126+ type t = {
127127+ created : date_time option;
128128+ author : string option;
129129+ architecture : Arch.t;
130130+ os : OS.t;
131131+ os_version : string option;
132132+ os_features : string list;
133133+ variant : Arch.variant option;
134134+ config : config option;
135135+ rootfs : rootfs;
136136+ history : history list;
137137+ }
138138+139139+ let jsont =
140140+ Jsont.Object.map ~kind:"oci_image_config"
141141+ (fun
142142+ created
143143+ author
144144+ architecture
145145+ os
146146+ os_version
147147+ os_features
148148+ variant
149149+ config
150150+ rootfs
151151+ history
152152+ ->
153153+ {
154154+ created;
155155+ author;
156156+ architecture;
157157+ os;
158158+ os_version;
159159+ os_features = Option.value ~default:[] os_features;
160160+ variant;
161161+ config;
162162+ rootfs;
163163+ history = Option.value ~default:[] history;
164164+ })
165165+ |> Jsont.Object.opt_mem "created" date_time_jsont ~enc:(fun t -> t.created)
166166+ |> Jsont.Object.opt_mem "author" Jsont.string ~enc:(fun t -> t.author)
167167+ |> Jsont.Object.mem "architecture" Arch.jsont ~enc:(fun t -> t.architecture)
168168+ |> Jsont.Object.mem "os" OS.jsont ~enc:(fun t -> t.os)
169169+ |> Jsont.Object.opt_mem "os.version" Jsont.string ~enc:(fun t ->
170170+ t.os_version)
171171+ |> Jsont.Object.opt_mem "os.features" (Jsont.list Jsont.string)
172172+ ~enc:(fun t -> if t.os_features = [] then None else Some t.os_features)
173173+ |> Jsont.Object.opt_mem "variant" Arch.variant_jsont ~enc:(fun t ->
174174+ t.variant)
175175+ |> Jsont.Object.opt_mem "config" config_jsont ~enc:(fun t -> t.config)
176176+ |> Jsont.Object.mem "rootfs" rootfs_jsont ~enc:(fun t -> t.rootfs)
177177+ |> Jsont.Object.opt_mem "history" (Jsont.list history_jsont) ~enc:(fun t ->
178178+ if t.history = [] then None else Some t.history)
179179+ |> Jsont.Object.skip_unknown |> Jsont.Object.finish
180180+181181+ let of_yojson json =
182182+ match Jsont_bytesrw.decode_string jsont (json_to_string json) with
183183+ | Ok t ->
184184+ if t.rootfs.type_ <> "layers" then Error "rootfs.type must be 'layers'"
185185+ else Ok t
186186+ | Error e -> Error e
187187+188188+ let to_yojson t =
189189+ match Jsont_bytesrw.encode_string jsont t with
190190+ | Ok s -> (
191191+ match json_of_string s with
192192+ | Ok j -> j
193193+ | Error _ -> Jsont.Null ((), Jsont.Meta.none))
194194+ | Error _ -> Jsont.Null ((), Jsont.Meta.none)
195195+196196+ let of_string str =
197197+ wrap
198198+ @@
199199+ let* json = json_of_string str in
200200+ of_yojson json
201201+202202+ let platform { os_version; os_features; variant; architecture; os; _ } =
203203+ Platform.v ?os_version ~os_features ?variant architecture os
204204+end
205205+206206+module Docker = struct
207207+ type config = {
208208+ hostname : string option;
209209+ domain_name : string option;
210210+ user : string option;
211211+ attach_stdin : bool;
212212+ attach_stdout : bool;
213213+ attach_stderr : bool;
214214+ exposed_ports : set;
215215+ tty : bool;
216216+ open_stdin : bool;
217217+ stdin_once : bool;
218218+ env : env list option;
219219+ cmd : string list option;
220220+ healthcheck : set;
221221+ args_escaped : bool option;
222222+ image : string option;
223223+ volumes : set;
224224+ working_dir : string;
225225+ entrypoint : string list option;
226226+ network_disabled : bool;
227227+ mac_address : string option;
228228+ on_build : string list option;
229229+ labels : (Annotation.t, string) map;
230230+ stop_signal : string option;
231231+ stop_timeout : int option;
232232+ shell : string list;
233233+ }
234234+235235+ let config_jsont =
236236+ Jsont.Object.map ~kind:"docker_config"
237237+ (fun
238238+ hostname
239239+ domain_name
240240+ user
241241+ attach_stdin
242242+ attach_stdout
243243+ attach_stderr
244244+ exposed_ports
245245+ tty
246246+ open_stdin
247247+ stdin_once
248248+ env
249249+ cmd
250250+ healthcheck
251251+ args_escaped
252252+ image
253253+ volumes
254254+ working_dir
255255+ entrypoint
256256+ network_disabled
257257+ mac_address
258258+ on_build
259259+ labels
260260+ stop_signal
261261+ stop_timeout
262262+ shell
263263+ ->
264264+ {
265265+ hostname;
266266+ domain_name;
267267+ user;
268268+ attach_stdin = Option.value ~default:false attach_stdin;
269269+ attach_stdout = Option.value ~default:false attach_stdout;
270270+ attach_stderr = Option.value ~default:false attach_stderr;
271271+ exposed_ports = Option.value ~default:[] exposed_ports;
272272+ tty = Option.value ~default:false tty;
273273+ open_stdin = Option.value ~default:false open_stdin;
274274+ stdin_once = Option.value ~default:false stdin_once;
275275+ env;
276276+ cmd;
277277+ healthcheck = Option.value ~default:[] healthcheck;
278278+ args_escaped;
279279+ image;
280280+ volumes = Option.value ~default:[] volumes;
281281+ working_dir = Option.value ~default:"" working_dir;
282282+ entrypoint;
283283+ network_disabled = Option.value ~default:false network_disabled;
284284+ mac_address;
285285+ on_build;
286286+ labels = Option.value ~default:[] labels;
287287+ stop_signal;
288288+ stop_timeout;
289289+ shell = Option.value ~default:[] shell;
290290+ })
291291+ |> Jsont.Object.opt_mem "Hostname" Jsont.string ~enc:(fun c -> c.hostname)
292292+ |> Jsont.Object.opt_mem "Domainname" Jsont.string ~enc:(fun c ->
293293+ c.domain_name)
294294+ |> Jsont.Object.opt_mem "User" Jsont.string ~enc:(fun c -> c.user)
295295+ |> Jsont.Object.opt_mem "AttachStdin" Jsont.bool ~enc:(fun c ->
296296+ if c.attach_stdin then Some true else None)
297297+ |> Jsont.Object.opt_mem "AttachStdout" Jsont.bool ~enc:(fun c ->
298298+ if c.attach_stdout then Some true else None)
299299+ |> Jsont.Object.opt_mem "AttachStderr" Jsont.bool ~enc:(fun c ->
300300+ if c.attach_stderr then Some true else None)
301301+ |> Jsont.Object.opt_mem "ExposedPorts" set_jsont ~enc:(fun c ->
302302+ if c.exposed_ports = [] then None else Some c.exposed_ports)
303303+ |> Jsont.Object.opt_mem "Tty" Jsont.bool ~enc:(fun c ->
304304+ if c.tty then Some true else None)
305305+ |> Jsont.Object.opt_mem "OpenStdin" Jsont.bool ~enc:(fun c ->
306306+ if c.open_stdin then Some true else None)
307307+ |> Jsont.Object.opt_mem "StdinOnce" Jsont.bool ~enc:(fun c ->
308308+ if c.stdin_once then Some true else None)
309309+ |> Jsont.Object.opt_mem "Env" (Jsont.list env_jsont) ~enc:(fun c -> c.env)
310310+ |> Jsont.Object.opt_mem "Cmd" (Jsont.list Jsont.string) ~enc:(fun c ->
311311+ c.cmd)
312312+ |> Jsont.Object.opt_mem "HealthCheck" set_jsont ~enc:(fun c ->
313313+ if c.healthcheck = [] then None else Some c.healthcheck)
314314+ |> Jsont.Object.opt_mem "ArgsEscaped" Jsont.bool ~enc:(fun c ->
315315+ c.args_escaped)
316316+ |> Jsont.Object.opt_mem "Image" Jsont.string ~enc:(fun c -> c.image)
317317+ |> Jsont.Object.opt_mem "Volumes" set_jsont ~enc:(fun c ->
318318+ if c.volumes = [] then None else Some c.volumes)
319319+ |> Jsont.Object.opt_mem "WorkingDir" Jsont.string ~enc:(fun c ->
320320+ if c.working_dir = "" then None else Some c.working_dir)
321321+ |> Jsont.Object.opt_mem "Entrypoint" (Jsont.list Jsont.string)
322322+ ~enc:(fun c -> c.entrypoint)
323323+ |> Jsont.Object.opt_mem "NetworkDisabled" Jsont.bool ~enc:(fun c ->
324324+ if c.network_disabled then Some true else None)
325325+ |> Jsont.Object.opt_mem "MacAddress" Jsont.string ~enc:(fun c ->
326326+ c.mac_address)
327327+ |> Jsont.Object.opt_mem "OnBuild" (Jsont.list Jsont.string) ~enc:(fun c ->
328328+ c.on_build)
329329+ |> Jsont.Object.opt_mem "Labels" labels_jsont ~enc:(fun c ->
330330+ if c.labels = [] then None else Some c.labels)
331331+ |> Jsont.Object.opt_mem "StopSignal" Jsont.string ~enc:(fun c ->
332332+ c.stop_signal)
333333+ |> Jsont.Object.opt_mem "StopTimeout" Jsont.int ~enc:(fun c ->
334334+ c.stop_timeout)
335335+ |> Jsont.Object.opt_mem "Shell" (Jsont.list Jsont.string) ~enc:(fun c ->
336336+ if c.shell = [] then None else Some c.shell)
337337+ |> Jsont.Object.skip_unknown |> Jsont.Object.finish
338338+339339+ type rootfs = { type_ : string; diff_ids : Digest.t list }
340340+341341+ let rootfs_jsont =
342342+ Jsont.Object.map ~kind:"rootfs" (fun type_ diff_ids -> { type_; diff_ids })
343343+ |> Jsont.Object.mem "type" Jsont.string ~enc:(fun r -> r.type_)
344344+ |> Jsont.Object.mem "diff_ids" (Jsont.list Digest.jsont) ~enc:(fun r ->
345345+ r.diff_ids)
346346+ |> Jsont.Object.finish
347347+348348+ type history = {
349349+ created : date_time option;
350350+ author : string option;
351351+ created_by : string option;
352352+ comment : string option;
353353+ empty_layer : bool;
354354+ }
355355+356356+ let history_jsont =
357357+ Jsont.Object.map ~kind:"history"
358358+ (fun created author created_by comment empty_layer ->
359359+ {
360360+ created;
361361+ author;
362362+ created_by;
363363+ comment;
364364+ empty_layer = Option.value ~default:false empty_layer;
365365+ })
366366+ |> Jsont.Object.opt_mem "created" date_time_jsont ~enc:(fun h -> h.created)
367367+ |> Jsont.Object.opt_mem "author" Jsont.string ~enc:(fun h -> h.author)
368368+ |> Jsont.Object.opt_mem "created_by" Jsont.string ~enc:(fun h ->
369369+ h.created_by)
370370+ |> Jsont.Object.opt_mem "comment" Jsont.string ~enc:(fun h -> h.comment)
371371+ |> Jsont.Object.opt_mem "empty_layer" Jsont.bool ~enc:(fun h ->
372372+ if h.empty_layer then Some true else None)
373373+ |> Jsont.Object.skip_unknown |> Jsont.Object.finish
374374+375375+ type t = {
376376+ id : string option;
377377+ parent : Digest.t option;
378378+ comment : string option;
379379+ created : date_time;
380380+ container : string option;
381381+ container_config : config option;
382382+ docker_version : string option;
383383+ author : string option;
384384+ config : config option;
385385+ architecture : Arch.t;
386386+ variant : Arch.variant option;
387387+ os : OS.t;
388388+ os_version : string option;
389389+ os_features : string list;
390390+ size : int64 option;
391391+ rootfs : rootfs;
392392+ history : history list;
393393+ }
394394+395395+ let jsont =
396396+ Jsont.Object.map ~kind:"docker_image_config"
397397+ (fun
398398+ id
399399+ parent
400400+ comment
401401+ created
402402+ container
403403+ container_config
404404+ docker_version
405405+ author
406406+ config
407407+ architecture
408408+ variant
409409+ os
410410+ os_version
411411+ os_features
412412+ size
413413+ rootfs
414414+ history
415415+ ->
416416+ {
417417+ id;
418418+ parent;
419419+ comment;
420420+ created;
421421+ container;
422422+ container_config;
423423+ docker_version;
424424+ author;
425425+ config;
426426+ architecture;
427427+ variant;
428428+ os;
429429+ os_version;
430430+ os_features = Option.value ~default:[] os_features;
431431+ size;
432432+ rootfs;
433433+ history = Option.value ~default:[] history;
434434+ })
435435+ |> Jsont.Object.opt_mem "id" Jsont.string ~enc:(fun t -> t.id)
436436+ |> Jsont.Object.opt_mem "parent" Digest.jsont ~enc:(fun t -> t.parent)
437437+ |> Jsont.Object.opt_mem "comment" Jsont.string ~enc:(fun t -> t.comment)
438438+ |> Jsont.Object.mem "created" date_time_jsont ~enc:(fun t -> t.created)
439439+ |> Jsont.Object.opt_mem "container" Jsont.string ~enc:(fun t -> t.container)
440440+ |> Jsont.Object.opt_mem "container_config" config_jsont ~enc:(fun t ->
441441+ t.container_config)
442442+ |> Jsont.Object.opt_mem "docker_version" Jsont.string ~enc:(fun t ->
443443+ t.docker_version)
444444+ |> Jsont.Object.opt_mem "author" Jsont.string ~enc:(fun t -> t.author)
445445+ |> Jsont.Object.opt_mem "config" config_jsont ~enc:(fun t -> t.config)
446446+ |> Jsont.Object.mem "architecture" Arch.jsont ~enc:(fun t -> t.architecture)
447447+ |> Jsont.Object.opt_mem "variant" Arch.variant_jsont ~enc:(fun t ->
448448+ t.variant)
449449+ |> Jsont.Object.mem "os" OS.jsont ~enc:(fun t -> t.os)
450450+ |> Jsont.Object.opt_mem "os.version" Jsont.string ~enc:(fun t ->
451451+ t.os_version)
452452+ |> Jsont.Object.opt_mem "os.features" (Jsont.list Jsont.string)
453453+ ~enc:(fun t -> if t.os_features = [] then None else Some t.os_features)
454454+ |> Jsont.Object.opt_mem "Size" Jsont.int64 ~enc:(fun t -> t.size)
455455+ |> Jsont.Object.mem "rootfs" rootfs_jsont ~enc:(fun t -> t.rootfs)
456456+ |> Jsont.Object.opt_mem "history" (Jsont.list history_jsont) ~enc:(fun t ->
457457+ if t.history = [] then None else Some t.history)
458458+ |> Jsont.Object.skip_unknown |> Jsont.Object.finish
459459+460460+ let of_yojson json =
461461+ match Jsont_bytesrw.decode_string jsont (json_to_string json) with
462462+ | Ok t ->
463463+ if t.rootfs.type_ <> "layers" then Error "rootfs.type must be 'layers'"
464464+ else Ok t
465465+ | Error e -> Error e
466466+467467+ let to_yojson t =
468468+ match Jsont_bytesrw.encode_string jsont t with
469469+ | Ok s -> (
470470+ match json_of_string s with
471471+ | Ok j -> j
472472+ | Error _ -> Jsont.Null ((), Jsont.Meta.none))
473473+ | Error _ -> Jsont.Null ((), Jsont.Meta.none)
474474+475475+ let pp ppf t = pp_json ppf (to_yojson t)
476476+477477+ let of_string str =
478478+ wrap
479479+ @@
480480+ let* json = json_of_string str in
481481+ of_yojson json
482482+483483+ let platform { os_version; os_features; variant; architecture; os; _ } =
484484+ Platform.v ?os_version ~os_features ?variant architecture os
485485+end
486486+487487+type t = OCI of OCI.t | Docker of Docker.t
488488+489489+let pp ppf = function
490490+ | OCI oci -> pp_json ppf (OCI.to_yojson oci)
491491+ | Docker docker -> pp_json ppf (Docker.to_yojson docker)
492492+493493+let env = function
494494+ | OCI oci -> (
495495+ match Option.map (fun (config : OCI.config) -> config.env) oci.config with
496496+ | None -> []
497497+ | Some v -> v)
498498+ | Docker docker -> (
499499+ match
500500+ Option.bind docker.config (fun (config : Docker.config) -> config.env)
501501+ with
502502+ | None -> []
503503+ | Some v -> v)
504504+505505+let platform = function
506506+ | OCI c -> OCI.platform c
507507+ | Docker d -> Docker.platform d
508508+509509+let of_string ~media_type str =
510510+ match media_type with
511511+ | Media_type.OCI Image_config ->
512512+ let+ c = OCI.of_string str in
513513+ OCI c
514514+ | Docker Image_config ->
515515+ let+ c = Docker.of_string str in
516516+ Docker c
517517+ | ty ->
518518+ error_msg "Config.of_string: %a is not a supported media type."
519519+ Media_type.pp ty
+35
src/spec/config.mli
···11+(** Configuration *)
22+33+module OCI : sig
44+ type config
55+ type t
66+77+ val jsont : t Jsont.t
88+ val of_yojson : Jsont.json -> (t, string) result
99+ val to_yojson : t -> Jsont.json
1010+ val of_string : string -> (t, [ `Msg of string ]) result
1111+ val platform : t -> Platform.t
1212+end
1313+1414+module Docker : sig
1515+ type t
1616+1717+ val jsont : t Jsont.t
1818+ val of_yojson : Jsont.json -> (t, string) result
1919+ val to_yojson : t -> Jsont.json
2020+ val pp : t Fmt.t
2121+ val of_string : string -> (t, [ `Msg of string ]) result
2222+ val platform : t -> Platform.t
2323+end
2424+2525+type t = OCI of OCI.t | Docker of Docker.t
2626+2727+val pp : t Fmt.t
2828+2929+val env : t -> (string * string) list
3030+(** Environment variables *)
3131+3232+val platform : t -> Platform.t
3333+3434+val of_string :
3535+ media_type:Media_type.t -> string -> (t, [ `Msg of string ]) result
+89
src/spec/content_type.ml
···11+open Astring
22+33+type t = {
44+ type' : string;
55+ facets : string list;
66+ suffix : string option;
77+ parameters : (string * string) list;
88+}
99+1010+exception Break of string
1111+1212+let break fmt = Fmt.kstr (fun s -> raise (Break s)) fmt
1313+1414+let check_name_first = function
1515+ | 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' -> ()
1616+ | _ -> break "rfc6838: name first"
1717+1818+let check_name_chars = function
1919+ | 'a' .. 'z'
2020+ | 'A' .. 'Z'
2121+ | '0' .. '9'
2222+ | '!' | '#' | '$' | '&' | '-' | '^' | '_' ->
2323+ ()
2424+ | _ -> break "rfc6838: name chars"
2525+2626+let check_name = function
2727+ | "" -> break "rfc6838: empty name"
2828+ | s ->
2929+ check_name_first s.[0];
3030+ if String.length s > 127 then
3131+ break "rfc6838: length of type (%d)" (String.length s);
3232+ String.iter check_name_chars s
3333+3434+let facet_of_string s =
3535+ let facets = String.cuts ~sep:"." s in
3636+ List.iter check_name facets;
3737+ facets
3838+3939+let subtype_of_string s =
4040+ match String.cut ~sep:"+" s with
4141+ | Some (f, suffix) ->
4242+ check_name suffix;
4343+ let fs = facet_of_string f in
4444+ (fs, Some suffix)
4545+ | None ->
4646+ let fs = facet_of_string s in
4747+ (fs, None)
4848+4949+let prefix_of_string s =
5050+ match String.cut ~sep:"/" s with
5151+ | Some (type', s) ->
5252+ check_name type';
5353+ let fs, suffix = subtype_of_string s in
5454+ (type', fs, suffix)
5555+ | None -> break "rfc6838: prefix"
5656+5757+let parameter_of_string s =
5858+ match String.cut ~sep:"=" s with
5959+ | Some (k, v) ->
6060+ check_name k;
6161+ (k, v)
6262+ | None -> break "rfc6838: parameter"
6363+6464+let of_string s =
6565+ match String.cuts ~sep:";" s with
6666+ | [] -> Error "rfc6838: empty"
6767+ | s :: ps -> (
6868+ try
6969+ let parameters = List.map parameter_of_string ps in
7070+ let type', facets, suffix = prefix_of_string s in
7171+ Ok { type'; facets; suffix; parameters }
7272+ with Break e -> Error e)
7373+7474+let to_string t =
7575+ let p =
7676+ t.type' ^ "/"
7777+ ^ String.concat ~sep:"." t.facets
7878+ ^ match t.suffix with None -> "" | Some s -> "+" ^ s
7979+ in
8080+ let ps = List.map (fun (k, v) -> k ^ "=" ^ v) t.parameters in
8181+ String.concat ~sep:"; " (p :: ps)
8282+8383+let jsont =
8484+ Jsont.map ~kind:"content_type"
8585+ ~dec:(fun s ->
8686+ match of_string s with
8787+ | Ok v -> v
8888+ | Error e -> Jsont.Error.msgf Jsont.Meta.none "invalid content_type: %s" e)
8989+ ~enc:to_string Jsont.string
+5
src/spec/content_type.mli
···11+type t
22+33+val jsont : t Jsont.t
44+val of_string : string -> (t, string) result
55+val to_string : t -> string
+127
src/spec/descriptor.ml
···11+open Common
22+33+module Uri_jsont = struct
44+ let jsont =
55+ Jsont.map ~kind:"uri"
66+ ~dec:(fun s ->
77+ let uri = Uri.of_string s in
88+ match Uri.scheme uri with
99+ | Some _ -> uri
1010+ | None ->
1111+ Jsont.Error.msgf Jsont.Meta.none "invalid URL: missing scheme in %s"
1212+ s)
1313+ ~enc:Uri.to_string Jsont.string
1414+end
1515+1616+type t = {
1717+ media_type : Media_type.t;
1818+ digest : Digest.t;
1919+ size : z;
2020+ urls : Uri.t list;
2121+ annotations : (Annotation.t, string) map;
2222+ data : Base64.t option;
2323+ platform : Platform.t option;
2424+ artifact_type : Content_type.t option;
2525+}
2626+2727+let annotations_jsont =
2828+ map_jsont
2929+ (fun s -> Ok (Annotation.of_string s))
3030+ Annotation.to_string Jsont.string
3131+3232+let jsont =
3333+ Jsont.Object.map ~kind:"descriptor"
3434+ (fun media_type digest size urls annotations data platform artifact_type ->
3535+ {
3636+ media_type;
3737+ digest;
3838+ size;
3939+ urls = Option.value ~default:[] urls;
4040+ annotations = Option.value ~default:[] annotations;
4141+ data;
4242+ platform;
4343+ artifact_type;
4444+ })
4545+ |> Jsont.Object.mem "mediaType" Media_type.jsont ~enc:(fun d -> d.media_type)
4646+ |> Jsont.Object.mem "digest" Digest.jsont ~enc:(fun d -> d.digest)
4747+ |> Jsont.Object.mem "size" z_jsont ~enc:(fun d -> d.size)
4848+ |> Jsont.Object.opt_mem "urls" (Jsont.list Uri_jsont.jsont) ~enc:(fun d ->
4949+ if d.urls = [] then None else Some d.urls)
5050+ |> Jsont.Object.opt_mem "annotations" annotations_jsont ~enc:(fun d ->
5151+ if d.annotations = [] then None else Some d.annotations)
5252+ |> Jsont.Object.opt_mem "data" Base64.jsont ~enc:(fun d -> d.data)
5353+ |> Jsont.Object.opt_mem "platform" Platform.jsont ~enc:(fun d -> d.platform)
5454+ |> Jsont.Object.opt_mem "artifactType" Content_type.jsont ~enc:(fun d ->
5555+ d.artifact_type)
5656+ |> Jsont.Object.finish
5757+5858+let v ?platform ?data ~media_type ~size digest =
5959+ let data =
6060+ match data with None -> None | Some d -> Some (Base64.encode d)
6161+ in
6262+ {
6363+ media_type;
6464+ size;
6565+ digest;
6666+ urls = [];
6767+ annotations = [];
6868+ data;
6969+ platform;
7070+ artifact_type = None;
7171+ }
7272+7373+let of_yojson json =
7474+ match Jsont_bytesrw.decode_string jsont (json_to_string json) with
7575+ | Ok t -> Ok t
7676+ | Error e -> Error e
7777+7878+let to_yojson t =
7979+ match Jsont_bytesrw.encode_string jsont t with
8080+ | Ok s -> (
8181+ match json_of_string s with
8282+ | Ok j -> j
8383+ | Error _ -> Jsont.Null ((), Jsont.Meta.none))
8484+ | Error _ -> Jsont.Null ((), Jsont.Meta.none)
8585+8686+let pp ppf t = pp_json ppf (to_yojson t)
8787+let to_string = Fmt.to_to_string pp
8888+let media_type t = t.media_type
8989+let platform t = t.platform
9090+9191+let empty =
9292+ {
9393+ media_type = OCI Empty;
9494+ size = Int63.of_int 2;
9595+ digest =
9696+ Digest.sha256
9797+ "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a";
9898+ data = Some (Base64.of_raw "e30=");
9999+ annotations = [];
100100+ urls = [];
101101+ platform = None;
102102+ artifact_type = None;
103103+ }
104104+105105+let size t = t.size
106106+let digest t = t.digest
107107+108108+let decoded_data t =
109109+ match t.data with None -> Error (`Msg "no data") | Some d -> Base64.decode d
110110+111111+let check t =
112112+ match t.data with
113113+ | None -> Ok ()
114114+ | Some data -> (
115115+ match Base64.decode data with
116116+ | Error e -> Error e
117117+ | Ok data ->
118118+ if t.size = Int63.of_int (String.length data) then
119119+ Digest.validate t.digest data
120120+ else
121121+ error_msg "Descriptor.check: invalid size: expected %a, got %d"
122122+ Int63.pp t.size (String.length data))
123123+124124+let attestation_manifest d =
125125+ match List.assoc_opt Annotation.Reference_type d.annotations with
126126+ | Some "attestation-manifest" -> true
127127+ | _ -> false
+27
src/spec/descriptor.mli
···11+open Common
22+open Optint
33+44+type t
55+66+val jsont : t Jsont.t
77+val of_yojson : Jsont.json -> (t, string) result
88+val to_yojson : t -> Jsont.json
99+1010+val v :
1111+ ?platform:Platform.t ->
1212+ ?data:string ->
1313+ media_type:Media_type.t ->
1414+ size:z ->
1515+ Oci_spec__Digest.t ->
1616+ t
1717+1818+val pp : t Fmt.t
1919+val to_string : t -> string
2020+val digest : t -> Digest.t
2121+val size : t -> Int63.t
2222+val empty : t
2323+val media_type : t -> Media_type.t
2424+val platform : t -> Platform.t option
2525+val decoded_data : t -> (string, [ `Msg of string ]) result
2626+val check : t -> (unit, [ `Msg of string ]) result
2727+val attestation_manifest : t -> bool
+161
src/spec/digest.ml
···11+open Common
22+open Astring
33+44+type algorithm = SHA256 | SHA512 | Unregistered of string list
55+type t = { algorithm : algorithm; encoded : string }
66+77+let algorithm t = t.algorithm
88+99+exception Break of string
1010+1111+let break fmt = Fmt.kstr (fun s -> raise (Break s)) fmt
1212+1313+let algorithm_of_string = function
1414+ | "" -> error_msg "Digest.algorithm_of_string: error - empty digest"
1515+ | "sha256" -> Ok SHA256
1616+ | "sha512" -> Ok SHA512
1717+ | s -> (
1818+ let l =
1919+ String.fields
2020+ ~is_sep:(function '+' | '.' | '_' | '-' -> true | _ -> false)
2121+ s
2222+ in
2323+ try
2424+ List.iter
2525+ (fun s ->
2626+ if s = "" then break "algorithm-component";
2727+ String.iter
2828+ (function
2929+ | 'a' .. 'z' | '0' .. '9' -> ()
3030+ | _ -> break "algorithm-component")
3131+ s)
3232+ l;
3333+ Ok (Unregistered l)
3434+ with Break e -> error_msg "Digest.algorithm_of_string: error - %s" e)
3535+3636+let string_of_algorithm = function
3737+ | SHA256 -> "sha256"
3838+ | SHA512 -> "sha512"
3939+ | Unregistered s -> String.concat ~sep:"+" s
4040+4141+let assert_hexa = function
4242+ | 'a' .. 'f' | '0' .. '9' -> ()
4343+ | c -> break "%c is not hexa-encoded" c
4444+4545+let assert_encoded = function
4646+ | 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '=' | '_' | '-' -> ()
4747+ | c -> break "%c is not encoded properly" c
4848+4949+let encoded_of_string algo e =
5050+ try
5151+ let () =
5252+ match algo with
5353+ | SHA256 ->
5454+ if String.length e <> 64 then break "invalid size";
5555+ String.iter assert_hexa e
5656+ | SHA512 ->
5757+ if String.length e <> 128 then break "invalid size";
5858+ String.iter assert_hexa e
5959+ | Unregistered _ -> String.iter assert_encoded e
6060+ in
6161+ Ok e
6262+ with Break e -> error_msg "Digest.encoded_of_string: invalid format (%S)" e
6363+6464+let v a e =
6565+ let+ e = encoded_of_string a e in
6666+ { algorithm = a; encoded = e }
6767+6868+let unsafe_v a e = { algorithm = a; encoded = e }
6969+let encoded_hash d = d.encoded
7070+7171+let of_string str =
7272+ match String.cut ~sep:":" str with
7373+ | None -> error_msg "Digest.of_string: %S does not contain ':'" str
7474+ | Some (a, e) -> (
7575+ match algorithm_of_string a with
7676+ | Ok a -> (
7777+ match encoded_of_string a e with
7878+ | Ok e -> Ok { algorithm = a; encoded = e }
7979+ | Error _ as e -> e)
8080+ | Error _ as e -> e)
8181+8282+let to_string t = string_of_algorithm t.algorithm ^ ":" ^ t.encoded
8383+let pp = Fmt.of_to_string to_string
8484+8585+let jsont =
8686+ Jsont.map ~kind:"digest"
8787+ ~dec:(fun s ->
8888+ match of_string s with
8989+ | Ok v -> v
9090+ | Error (`Msg e) ->
9191+ Jsont.Error.msgf Jsont.Meta.none "invalid digest: %s" e)
9292+ ~enc:to_string Jsont.string
9393+9494+let equal x y =
9595+ x == y || (x.algorithm = y.algorithm && String.equal x.encoded y.encoded)
9696+9797+let sha256 s =
9898+ match encoded_of_string SHA256 s with
9999+ | Ok e -> { algorithm = SHA256; encoded = e }
100100+ | Error (`Msg e) -> invalid_arg e
101101+102102+let sha512 s =
103103+ match encoded_of_string SHA512 s with
104104+ | Ok e -> { algorithm = SHA512; encoded = e }
105105+ | Error (`Msg e) -> invalid_arg e
106106+107107+let validation_error a to_hex ~got ~expected =
108108+ let a = string_of_algorithm a in
109109+ error_msg "Digest.validate: validation error, got %s:%s, expected %s:%s" a
110110+ (to_hex got) a (to_hex expected)
111111+112112+let unregistered_error ds =
113113+ error_msg "Digest.validate: unregistered algorithms %a"
114114+ Fmt.(Dump.list string)
115115+ ds
116116+117117+let validate t buf =
118118+ match t.algorithm with
119119+ | SHA256 ->
120120+ let expected = Digestif.SHA256.of_hex t.encoded in
121121+ let got = Digestif.SHA256.digest_string buf in
122122+ if Digestif.SHA256.equal got expected then Ok ()
123123+ else validation_error SHA256 Digestif.SHA256.to_hex ~got ~expected
124124+ | SHA512 ->
125125+ let expected = Digestif.SHA512.of_hex t.encoded in
126126+ let got = Digestif.SHA512.digest_string buf in
127127+ if Digestif.SHA512.equal got expected then Ok ()
128128+ else validation_error SHA512 Digestif.SHA512.to_hex ~got ~expected
129129+ | Unregistered ds -> unregistered_error ds
130130+131131+let digest_string algo str =
132132+ let encoded =
133133+ match algo with
134134+ | SHA256 -> Digestif.SHA256.(to_hex (digest_string str))
135135+ | SHA512 -> Digestif.SHA512.(to_hex (digest_string str))
136136+ | _ -> invalid_arg "digest_string"
137137+ in
138138+ unsafe_v algo encoded
139139+140140+let chain algo = function
141141+ | [] -> []
142142+ | h :: t ->
143143+ let _, l =
144144+ List.fold_left
145145+ (fun (h, acc) l ->
146146+ let str = to_string h ^ " " ^ to_string l in
147147+ let h' = digest_string algo str in
148148+ (h', h' :: acc))
149149+ (h, [ h ]) t
150150+ in
151151+ List.rev l
152152+153153+let chain_id algo = function
154154+ | [] -> invalid_arg "chain_id: empty list"
155155+ | h :: t ->
156156+ List.fold_left
157157+ (fun h l ->
158158+ let str = to_string h ^ " " ^ to_string l in
159159+ let h' = digest_string algo str in
160160+ h')
161161+ h t
+20
src/spec/digest.mli
···11+type algorithm = SHA256 | SHA512 | Unregistered of string list
22+type t
33+44+val jsont : t Jsont.t
55+val v : algorithm -> string -> (t, [ `Msg of string ]) result
66+val unsafe_v : algorithm -> string -> t
77+val algorithm : t -> algorithm
88+val string_of_algorithm : algorithm -> string
99+val algorithm_of_string : string -> (algorithm, [ `Msg of string ]) result
1010+val sha256 : string -> t
1111+val sha512 : string -> t
1212+val validate : t -> string -> (unit, [ `Msg of string ]) result
1313+val pp : t Fmt.t
1414+val to_string : t -> string
1515+val of_string : string -> (t, [ `Msg of string ]) result
1616+val equal : t -> t -> bool
1717+val chain : algorithm -> t list -> t list
1818+val chain_id : algorithm -> t list -> t
1919+val encoded_hash : t -> string
2020+val digest_string : algorithm -> string -> t
···11+open Common
22+33+let file = "oci-layout"
44+let version = "1.0.0"
55+let index = "index.json"
66+let blobs = "blobs"
77+88+type t = { version : int }
99+1010+let jsont =
1111+ Jsont.Object.map ~kind:"layout" (fun version -> { version })
1212+ |> Jsont.Object.mem "imageLayoutVersion" Jsont.int ~enc:(fun l -> l.version)
1313+ |> Jsont.Object.finish
1414+1515+let of_yojson json =
1616+ match Jsont_bytesrw.decode_string jsont (json_to_string json) with
1717+ | Ok t -> Ok t
1818+ | Error e -> Error e
1919+2020+let to_yojson t =
2121+ match Jsont_bytesrw.encode_string jsont t with
2222+ | Ok s -> (
2323+ match json_of_string s with
2424+ | Ok j -> j
2525+ | Error _ -> Jsont.Null ((), Jsont.Meta.none))
2626+ | Error _ -> Jsont.Null ((), Jsont.Meta.none)
+9
src/spec/layout.mli
···11+type t
22+33+val jsont : t Jsont.t
44+val of_yojson : Jsont.json -> (t, string) result
55+val to_yojson : t -> Jsont.json
66+val version : string
77+val file : string
88+val index : string
99+val blobs : string
+225
src/spec/manifest.ml
···11+open Common
22+33+let annotations_jsont =
44+ map_jsont
55+ (fun s -> Ok (Annotation.of_string s))
66+ Annotation.to_string Jsont.string
77+88+module OCI = struct
99+ type t = {
1010+ version : v2;
1111+ artifact_type : string option;
1212+ config : Descriptor.t;
1313+ layers : Descriptor.t list;
1414+ subject : Descriptor.t option;
1515+ annotations : (Annotation.t, string) map;
1616+ }
1717+ [@@warning "-69"]
1818+1919+ let jsont =
2020+ Jsont.Object.map ~kind:"oci_manifest"
2121+ (fun
2222+ version _media_type artifact_type config layers subject annotations ->
2323+ {
2424+ version;
2525+ artifact_type;
2626+ config;
2727+ layers;
2828+ subject;
2929+ annotations = Option.value ~default:[] annotations;
3030+ })
3131+ |> Jsont.Object.mem "schemaVersion" v2_jsont ~enc:(fun _ -> V2)
3232+ |> Jsont.Object.mem "mediaType" Jsont.string ~enc:(fun _ ->
3333+ Media_type.to_string (OCI Image_manifest))
3434+ |> Jsont.Object.opt_mem "artifactType" Jsont.string ~enc:(fun t ->
3535+ t.artifact_type)
3636+ |> Jsont.Object.mem "config" Descriptor.jsont ~enc:(fun t -> t.config)
3737+ |> Jsont.Object.mem "layers" (Jsont.list Descriptor.jsont) ~enc:(fun t ->
3838+ t.layers)
3939+ |> Jsont.Object.opt_mem "subject" Descriptor.jsont ~enc:(fun t -> t.subject)
4040+ |> Jsont.Object.opt_mem "annotations" annotations_jsont ~enc:(fun t ->
4141+ if t.annotations = [] then None else Some t.annotations)
4242+ |> Jsont.Object.finish
4343+4444+ let of_yojson json =
4545+ match Jsont_bytesrw.decode_string jsont (json_to_string json) with
4646+ | Ok t -> (
4747+ (* Validate the manifest *)
4848+ let check () =
4949+ match Descriptor.media_type t.config with
5050+ | OCI Empty -> (
5151+ match t.artifact_type with
5252+ | None ->
5353+ Error
5454+ "artifactType MUST be set when config.mediaType is set to \
5555+ the empty value."
5656+ | Some _ -> Ok ())
5757+ | _ -> Ok ()
5858+ in
5959+ let check_layers () =
6060+ match t.layers with
6161+ | [] ->
6262+ Error "For portability, layers SHOULD have at least one entry."
6363+ | _ -> Ok ()
6464+ in
6565+ match check () with
6666+ | Error e -> Error e
6767+ | Ok () -> (
6868+ match check_layers () with Error e -> Error e | Ok () -> Ok t))
6969+ | Error e -> Error e
7070+7171+ let to_yojson t =
7272+ match Jsont_bytesrw.encode_string jsont t with
7373+ | Ok s -> (
7474+ match json_of_string s with
7575+ | Ok j -> j
7676+ | Error _ -> Jsont.Null ((), Jsont.Meta.none))
7777+ | Error _ -> Jsont.Null ((), Jsont.Meta.none)
7878+7979+ let pp ppf t = pp_json ppf (to_yojson t)
8080+ let to_string = Fmt.to_to_string pp
8181+ let media_type _ = Media_type.OCI.Image_manifest
8282+ let layers t = t.layers
8383+ let config t = t.config
8484+8585+ let size t =
8686+ List.fold_left
8787+ (fun acc d -> Int63.add acc (Descriptor.size d))
8888+ (Descriptor.size t.config) t.layers
8989+9090+ let of_string s =
9191+ wrap
9292+ @@ let* json = json_of_string s in
9393+ of_yojson json
9494+end
9595+9696+module Docker = struct
9797+ type t = { version : v2; config : Descriptor.t; layers : Descriptor.t list }
9898+ [@@warning "-69"]
9999+100100+ let jsont =
101101+ Jsont.Object.map ~kind:"docker_manifest"
102102+ (fun version _media_type config layers -> { version; config; layers })
103103+ |> Jsont.Object.mem "schemaVersion" v2_jsont ~enc:(fun _ -> V2)
104104+ |> Jsont.Object.mem "mediaType" Jsont.string ~enc:(fun _ ->
105105+ Media_type.to_string (Docker Image_manifest))
106106+ |> Jsont.Object.mem "config" Descriptor.jsont ~enc:(fun t -> t.config)
107107+ |> Jsont.Object.mem "layers" (Jsont.list Descriptor.jsont) ~enc:(fun t ->
108108+ t.layers)
109109+ |> Jsont.Object.finish
110110+111111+ let of_yojson json =
112112+ match Jsont_bytesrw.decode_string jsont (json_to_string json) with
113113+ | Ok t -> Ok t
114114+ | Error e -> Error e
115115+116116+ let to_yojson t =
117117+ match Jsont_bytesrw.encode_string jsont t with
118118+ | Ok s -> (
119119+ match json_of_string s with
120120+ | Ok j -> j
121121+ | Error _ -> Jsont.Null ((), Jsont.Meta.none))
122122+ | Error _ -> Jsont.Null ((), Jsont.Meta.none)
123123+124124+ let layers t = t.layers
125125+ let config t = t.config
126126+ let pp ppf t = pp_json ppf (to_yojson t)
127127+ let to_string = Fmt.to_to_string pp
128128+ let media_type _ = Media_type.Docker.Image_manifest
129129+130130+ let size t =
131131+ List.fold_left
132132+ (fun acc d -> Int63.add acc (Descriptor.size d))
133133+ (Descriptor.size t.config) t.layers
134134+135135+ let of_string s =
136136+ wrap
137137+ @@ let* json = json_of_string s in
138138+ of_yojson json
139139+end
140140+141141+type t =
142142+ [ `Docker_manifest of Docker.t
143143+ | `Docker_manifest_list of Manifest_list.t
144144+ | `OCI_index of Index.t
145145+ | `OCI_manifest of OCI.t ]
146146+147147+let docker_manifest json =
148148+ let+ m = Docker.of_yojson json in
149149+ `Docker_manifest m
150150+151151+let docker_manifest_list json =
152152+ let+ m = Manifest_list.of_yojson json in
153153+ `Docker_manifest_list m
154154+155155+let oci_index json =
156156+ let+ m = Index.of_yojson json in
157157+ `OCI_index m
158158+159159+let oci_manifest json =
160160+ let+ m = OCI.of_yojson json in
161161+ `OCI_manifest m
162162+163163+let of_yojson json =
164164+ let str = json_to_string json in
165165+ let media_type =
166166+ match Media_type.guess str with
167167+ | Some mt -> mt
168168+ | None -> Media_type.OCI Image_index
169169+ in
170170+ match media_type with
171171+ | Docker Image_manifest -> docker_manifest json
172172+ | Docker Image_manifest_list -> docker_manifest_list json
173173+ | OCI Image_index -> oci_index json
174174+ | OCI Image_manifest -> oci_manifest json
175175+ | m -> error "Manifest.of_yojson: invalid media-type: %a" Media_type.pp m
176176+177177+let of_string body =
178178+ wrap
179179+ @@
180180+ let* json = json_of_string body in
181181+ of_yojson json
182182+183183+let to_string = function
184184+ | `Docker_manifest m -> Docker.to_string m
185185+ | `Docker_manifest_list l -> Manifest_list.to_string l
186186+ | `OCI_index i -> Index.to_string i
187187+ | `OCI_manifest m -> OCI.to_string m
188188+189189+let to_yojson = function
190190+ | `Docker_manifest m -> Docker.to_yojson m
191191+ | `Docker_manifest_list l -> Manifest_list.to_yojson l
192192+ | `OCI_index i -> Index.to_yojson i
193193+ | `OCI_manifest m -> OCI.to_yojson m
194194+195195+let pp = Fmt.of_to_string to_string
196196+197197+let size = function
198198+ | `Docker_manifest m -> Some (Docker.size m)
199199+ | `Docker_manifest_list _ -> None
200200+ | `OCI_index _ -> None
201201+ | `OCI_manifest m -> Some (OCI.size m)
202202+203203+let media_type = function
204204+ | `Docker_manifest _ -> Media_type.Docker Image_manifest
205205+ | `Docker_manifest_list _ -> Media_type.Docker Image_manifest_list
206206+ | `OCI_index _ -> Media_type.OCI Image_index
207207+ | `OCI_manifest _ -> Media_type.OCI Image_manifest
208208+209209+let platform read = function
210210+ | `Docker_manifest m ->
211211+ let config = Docker.config m in
212212+ let c = read config in
213213+ Some (Config.platform c)
214214+ | `Docker_manifest_list _ -> None
215215+ | `OCI_index i -> Index.platform i
216216+ | `OCI_manifest m ->
217217+ let config = OCI.config m in
218218+ let c = read config in
219219+ Some (Config.platform c)
220220+221221+let manifests = function
222222+ | `Docker_manifest _ -> []
223223+ | `Docker_manifest_list l -> Manifest_list.manifests l
224224+ | `OCI_index i -> Index.manifests i
225225+ | `OCI_manifest _ -> []
+45
src/spec/manifest.mli
···11+open Optint
22+33+module OCI : sig
44+ type t
55+66+ val jsont : t Jsont.t
77+ val of_yojson : Jsont.json -> (t, string) result
88+ val to_yojson : t -> Jsont.json
99+ val pp : t Fmt.t
1010+ val of_string : string -> (t, [ `Msg of string ]) result
1111+ val to_string : t -> string
1212+ val media_type : t -> Media_type.OCI.t
1313+ val config : t -> Descriptor.t
1414+ val layers : t -> Descriptor.t list
1515+end
1616+1717+module Docker : sig
1818+ type t
1919+2020+ val jsont : t Jsont.t
2121+ val of_yojson : Jsont.json -> (t, string) result
2222+ val to_yojson : t -> Jsont.json
2323+ val pp : t Fmt.t
2424+ val of_string : string -> (t, [ `Msg of string ]) result
2525+ val to_string : t -> string
2626+ val config : t -> Descriptor.t
2727+ val layers : t -> Descriptor.t list
2828+ val media_type : t -> Media_type.Docker.t
2929+end
3030+3131+type t =
3232+ [ `Docker_manifest of Docker.t
3333+ | `Docker_manifest_list of Manifest_list.t
3434+ | `OCI_index of Index.t
3535+ | `OCI_manifest of OCI.t ]
3636+3737+val of_yojson : Jsont.json -> (t, string) result
3838+val to_yojson : t -> Jsont.json
3939+val pp : t Fmt.t
4040+val to_string : t -> string
4141+val of_string : string -> (t, [ `Msg of string ]) result
4242+val size : t -> Int63.t option
4343+val media_type : t -> Media_type.t
4444+val platform : (Descriptor.t -> Config.t) -> t -> Platform.t option
4545+val manifests : t -> Descriptor.t list
+30
src/spec/manifest_list.ml
···11+open Common
22+33+type t = { version : v2; manifests : Descriptor.t list } [@@warning "-69"]
44+55+let jsont =
66+ Jsont.Object.map ~kind:"manifest_list" (fun version _media_type manifests ->
77+ { version; manifests })
88+ |> Jsont.Object.mem "schemaVersion" v2_jsont ~enc:(fun _ -> V2)
99+ |> Jsont.Object.mem "mediaType" Jsont.string ~enc:(fun _ ->
1010+ Media_type.to_string (Docker Image_manifest_list))
1111+ |> Jsont.Object.mem "manifests" (Jsont.list Descriptor.jsont) ~enc:(fun t ->
1212+ t.manifests)
1313+ |> Jsont.Object.finish
1414+1515+let of_yojson json =
1616+ match Jsont_bytesrw.decode_string jsont (json_to_string json) with
1717+ | Ok t -> Ok t
1818+ | Error e -> Error e
1919+2020+let to_yojson t =
2121+ match Jsont_bytesrw.encode_string jsont t with
2222+ | Ok s -> (
2323+ match json_of_string s with
2424+ | Ok j -> j
2525+ | Error _ -> Jsont.Null ((), Jsont.Meta.none))
2626+ | Error _ -> Jsont.Null ((), Jsont.Meta.none)
2727+2828+let pp ppf t = pp_json ppf (to_yojson t)
2929+let to_string = Fmt.to_to_string pp
3030+let manifests t = t.manifests
+8
src/spec/manifest_list.mli
···11+type t
22+33+val jsont : t Jsont.t
44+val of_yojson : Jsont.json -> (t, string) result
55+val to_yojson : t -> Jsont.json
66+val pp : t Fmt.t
77+val to_string : t -> string
88+val manifests : t -> Descriptor.t list
+133
src/spec/media_type.ml
···11+open Common
22+module Content_type = Content_type
33+44+module OCI = struct
55+ type t =
66+ | Empty
77+ | Descriptor
88+ | Layout_header
99+ | Image_index
1010+ | Image_manifest
1111+ | Image_config
1212+ | Layer_tar
1313+ | Layer_tar_gzip
1414+ | Layer_tar_zstd
1515+ | Layer_non_distributable_tar
1616+ | Layer_non_distributable_tar_gzip
1717+ | Layer_non_distributable_tar_zstd
1818+ | Trust
1919+ | Other of Content_type.t
2020+2121+ let of_string = function
2222+ | "application/vnd.oci.descriptor.v1+json" -> Ok Descriptor
2323+ | "application/vnd.oci.layout.header.v1+json" -> Ok Layout_header
2424+ | "application/vnd.oci.image.index.v1+json" -> Ok Image_index
2525+ | "application/vnd.oci.image.manifest.v1+json" -> Ok Image_manifest
2626+ | "application/vnd.oci.image.config.v1+json" -> Ok Image_config
2727+ | "application/vnd.oci.image.layer.v1.tar" -> Ok Layer_tar
2828+ | "application/vnd.oci.image.layer.v1.tar+gzip" -> Ok Layer_tar_gzip
2929+ | "application/vnd.oci.image.layer.v1.tar+zstd" -> Ok Layer_tar_zstd
3030+ | "application/vnd.oci.empty.v1+json" -> Ok Empty
3131+ | "application/vnd.oci.image.layer.nondistributable.v1.tar" ->
3232+ Ok Layer_non_distributable_tar
3333+ | "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip" ->
3434+ Ok Layer_non_distributable_tar_gzip
3535+ | "application/vnd.oci.image.layer.nondistributable.v1.tar+zstd" ->
3636+ Ok Layer_non_distributable_tar_zstd
3737+ | "application/vnd.in-toto+json" -> Ok Trust
3838+ | s ->
3939+ let+ s = Content_type.of_string s in
4040+ Other s
4141+4242+ let to_string = function
4343+ | Descriptor -> "application/vnd.oci.descriptor.v1+json"
4444+ | Layout_header -> "application/vnd.oci.layout.header.v1+json"
4545+ | Image_index -> "application/vnd.oci.image.index.v1+json"
4646+ | Image_manifest -> "application/vnd.oci.image.manifest.v1+json"
4747+ | Image_config -> "application/vnd.oci.image.config.v1+json"
4848+ | Layer_tar -> "application/vnd.oci.image.layer.v1.tar"
4949+ | Layer_tar_gzip -> "application/vnd.oci.image.layer.v1.tar+gzip"
5050+ | Layer_tar_zstd -> "application/vnd.oci.image.layer.v1.tar+zstd"
5151+ | Empty -> "application/vnd.oci.empty.v1+json"
5252+ | Layer_non_distributable_tar ->
5353+ "application/vnd.oci.image.layer.nondistributable.v1.tar"
5454+ | Layer_non_distributable_tar_gzip ->
5555+ "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip"
5656+ | Layer_non_distributable_tar_zstd ->
5757+ "application/vnd.oci.image.layer.nondistributable.v1.tar+zstd"
5858+ | Trust -> "application/vnd.in-toto+json"
5959+ | Other e -> Content_type.to_string e
6060+end
6161+6262+module Docker = struct
6363+ type t =
6464+ | Image_manifest
6565+ | Image_manifest_list
6666+ | Image_config
6767+ | Layer_tar_gzip
6868+ | Layer_non_distributable_tar_gzip
6969+ | Plugin_config
7070+7171+ let of_string = function
7272+ | "application/vnd.docker.distribution.manifest.v2+json" ->
7373+ Some Image_manifest
7474+ | "application/vnd.docker.distribution.manifest.list.v2+json" ->
7575+ Some Image_manifest_list
7676+ | "application/vnd.docker.container.image.v1+json" -> Some Image_config
7777+ | "application/vnd.docker.image.rootfs.diff.tar.gzip" -> Some Layer_tar_gzip
7878+ | "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip" ->
7979+ Some Layer_non_distributable_tar_gzip
8080+ | "application/vnd.docker.plugin.v1+json" -> Some Plugin_config
8181+ | _ -> None
8282+8383+ let to_string = function
8484+ | Image_manifest -> "application/vnd.docker.distribution.manifest.v2+json"
8585+ | Image_manifest_list ->
8686+ "application/vnd.docker.distribution.manifest.list.v2+json"
8787+ | Image_config -> "application/vnd.docker.container.image.v1+json"
8888+ | Layer_tar_gzip -> "application/vnd.docker.image.rootfs.diff.tar.gzip"
8989+ | Layer_non_distributable_tar_gzip ->
9090+ "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip"
9191+ | Plugin_config -> "application/vnd.docker.plugin.v1+json"
9292+end
9393+9494+type t = OCI of OCI.t | Docker of Docker.t
9595+9696+let of_string str =
9797+ match Docker.of_string str with
9898+ | Some t -> Ok (Docker t)
9999+ | None -> (
100100+ match OCI.of_string str with
101101+ | Ok t -> Ok (OCI t)
102102+ | Error e -> Error (`Msg e))
103103+104104+let to_string = function
105105+ | Docker t -> Docker.to_string t
106106+ | OCI t -> OCI.to_string t
107107+108108+let jsont =
109109+ Jsont.map ~kind:"media_type"
110110+ ~dec:(fun s ->
111111+ match of_string s with
112112+ | Ok t -> t
113113+ | Error (`Msg e) ->
114114+ Jsont.Error.msgf Jsont.Meta.none "invalid media_type: %s" e)
115115+ ~enc:to_string Jsont.string
116116+117117+let media_type_extractor =
118118+ Jsont.Object.map ~kind:"media_type_extractor" (fun media_type -> media_type)
119119+ |> Jsont.Object.opt_mem "mediaType" Jsont.string ~enc:(fun _ -> None)
120120+ |> Jsont.Object.skip_unknown |> Jsont.Object.finish
121121+122122+let guess str =
123123+ if str = "" then None
124124+ else
125125+ match str.[0] with
126126+ | '{' -> (
127127+ match Jsont_bytesrw.decode_string media_type_extractor str with
128128+ | Ok (Some s) -> (
129129+ match of_string s with Ok r -> Some r | Error _ -> None)
130130+ | _ -> None)
131131+ | _ -> None
132132+133133+let pp = Fmt.of_to_string to_string
+41
src/spec/media_type.mli
···11+module Content_type = Content_type
22+33+module OCI : sig
44+ type t =
55+ | Empty
66+ | Descriptor
77+ | Layout_header
88+ | Image_index
99+ | Image_manifest
1010+ | Image_config
1111+ | Layer_tar
1212+ | Layer_tar_gzip
1313+ | Layer_tar_zstd
1414+ | Layer_non_distributable_tar
1515+ | Layer_non_distributable_tar_gzip
1616+ | Layer_non_distributable_tar_zstd
1717+ | Trust
1818+ | Other of Content_type.t
1919+2020+ val to_string : t -> string
2121+end
2222+2323+module Docker : sig
2424+ type t =
2525+ | Image_manifest
2626+ | Image_manifest_list
2727+ | Image_config
2828+ | Layer_tar_gzip
2929+ | Layer_non_distributable_tar_gzip
3030+ | Plugin_config
3131+3232+ val to_string : t -> string
3333+end
3434+3535+type t = OCI of OCI.t | Docker of Docker.t
3636+3737+val jsont : t Jsont.t
3838+val pp : t Fmt.t
3939+val to_string : t -> string
4040+val of_string : string -> (t, [ `Msg of string ]) result
4141+val guess : string -> t option
+31
src/spec/oci_spec.ml
···11+module Common = Common
22+module Config = Config
33+module Descriptor = Descriptor
44+module Index = Index
55+module Annotation = Annotation
66+module Digest = Digest
77+module Manifest = Manifest
88+module Manifest_list = Manifest_list
99+module Layer = Layer
1010+module Media_type = Media_type
1111+module Blob = Blob
1212+module Auth = Auth
1313+module Platform = Platform
1414+module OS = OS
1515+module Arch = Arch
1616+1717+type oci = {
1818+ manifest : Manifest.OCI.t;
1919+ index : Index.t option;
2020+ layers : Layer.t list;
2121+ config : Config.OCI.t;
2222+}
2323+2424+let manifest t = t.manifest
2525+let index t = t.index
2626+let layers t = t.layers
2727+let config t = t.config
2828+2929+type docker = { manifest_list : Manifest_list.t }
3030+3131+let manifest_list t = t.manifest_list
+52
src/spec/oci_spec.mli
···11+(** {1 Container Image Spec}
22+33+ This module provides converters from OCI and Docker image specifications to
44+ OCaml data types. It abstracts the details of each image specification to
55+ allow easy integration and manipulation in OCaml applications.
66+77+ Users of this module can expect types and sub-modules representing each of
88+ the main components of OCI and Docker image specifications. *)
99+1010+(** {1 Types} *)
1111+1212+module Common = Common
1313+module Config = Config
1414+module Descriptor = Descriptor
1515+module Index = Index
1616+module Annotation = Annotation
1717+module Digest = Digest
1818+module Manifest = Manifest
1919+module Manifest_list = Manifest_list
2020+module Layer = Layer
2121+module Media_type = Media_type
2222+module Blob = Blob
2323+module Auth = Auth
2424+module Platform = Platform
2525+module OS = OS
2626+module Arch = Arch
2727+2828+type oci
2929+(** The type for OCI images as described in the
3030+ {{:https://github.com/opencontainers/image-spec/blob/main/spec.md} Image
3131+ Format Specification} of the
3232+ {{:https://opencontainers.org/} Open Container Initative}. *)
3333+3434+val manifest : oci -> Manifest.OCI.t
3535+(** [manifest img] is the manifest of the OCI image [img]. *)
3636+3737+val index : oci -> Index.t option
3838+(** [index img] is the optional index of the OCI image [img]. *)
3939+4040+val layers : oci -> Layer.t list
4141+(** [layers img] is the list of layers of the OCI image [img]. *)
4242+4343+val config : oci -> Config.OCI.t
4444+(** [config img] is the configuration of the OCI image [img]. *)
4545+4646+type docker
4747+(** The type for Docker images as specified by the
4848+ {{:https://github.com/moby/moby/blob/master/image/spec/spec.md} Docker Image
4949+ Specification v1.3} *)
5050+5151+val manifest_list : docker -> Manifest_list.t
5252+(** [manifest_list img] is the manifest list of the Docker image [img]. *)
+165
src/spec/platform.ml
···11+open Common
22+open Astring
33+44+type t = {
55+ architecture : Arch.t;
66+ os : OS.t;
77+ os_version : string option;
88+ os_features : string list;
99+ variant : Arch.variant option;
1010+ features : string list;
1111+}
1212+1313+let jsont =
1414+ Jsont.Object.map ~kind:"platform"
1515+ (fun architecture os os_version os_features variant features ->
1616+ {
1717+ architecture;
1818+ os;
1919+ os_version;
2020+ os_features = Option.value ~default:[] os_features;
2121+ variant;
2222+ features = Option.value ~default:[] features;
2323+ })
2424+ |> Jsont.Object.mem "architecture" Arch.jsont ~enc:(fun p -> p.architecture)
2525+ |> Jsont.Object.mem "os" OS.jsont ~enc:(fun p -> p.os)
2626+ |> Jsont.Object.opt_mem "os.version" Jsont.string ~enc:(fun p -> p.os_version)
2727+ |> Jsont.Object.opt_mem "os.features" (Jsont.list Jsont.string) ~enc:(fun p ->
2828+ if p.os_features = [] then None else Some p.os_features)
2929+ |> Jsont.Object.opt_mem "variant" Arch.variant_jsont ~enc:(fun p -> p.variant)
3030+ |> Jsont.Object.opt_mem "features" (Jsont.list Jsont.string) ~enc:(fun p ->
3131+ if p.features = [] then None else Some p.features)
3232+ |> Jsont.Object.finish
3333+3434+let v ?os_version ?(os_features = []) ?variant architecture os =
3535+ { architecture; os; os_version; os_features; variant; features = [] }
3636+3737+let arch t = t.architecture
3838+let os t = t.os
3939+let unknown = v Unknown Unknown
4040+4141+let of_string str =
4242+ match String.cuts ~sep:"/" str with
4343+ | [ os; arch ] ->
4444+ let* os = OS.of_string os in
4545+ let+ architecture = Arch.of_string arch in
4646+ {
4747+ os;
4848+ architecture;
4949+ os_version = None;
5050+ os_features = [];
5151+ variant = None;
5252+ features = [];
5353+ }
5454+ | [ os; arch; variant ] ->
5555+ let* os = OS.of_string os in
5656+ let* architecture = Arch.of_string arch in
5757+ let+ variant = Arch.variant_of_string variant in
5858+ {
5959+ os;
6060+ architecture;
6161+ os_version = None;
6262+ os_features = [];
6363+ variant = Some variant;
6464+ features = [];
6565+ }
6666+ | _ -> error_msg "Platform.of_string: invalid string (%S)" str
6767+6868+let pp ppf t =
6969+ match t.variant with
7070+ | None -> Fmt.pf ppf "%a/%a" OS.pp t.os Arch.pp t.architecture
7171+ | Some v ->
7272+ Fmt.pf ppf "%a/%a/%a" OS.pp t.os Arch.pp t.architecture Arch.pp_variant v
7373+7474+let to_string = Fmt.to_to_string pp
7575+let pp_v = Fmt.option ~none:(Fmt.any "N/A") Arch.pp_variant
7676+7777+let dump ppf t =
7878+ match Jsont_bytesrw.encode_string jsont t with
7979+ | Ok s -> (
8080+ match json_of_string s with
8181+ | Ok j -> pp_json ppf j
8282+ | Error e -> Fmt.pf ppf "<error: %s>" e)
8383+ | Error e -> Fmt.pf ppf "<error: %s>" e
8484+8585+let err_arch_variant t =
8686+ Fmt.failwith "%a/%a: invalid architecture/variant pair" Arch.pp t.architecture
8787+ pp_v t.variant
8888+8989+let err_os_arch t =
9090+ Fmt.failwith "%a/%a: invalid os/architecture pair" OS.pp t.os Arch.pp
9191+ t.architecture
9292+9393+let check t =
9494+ let () =
9595+ match (t.os, t.os_features) with
9696+ | Windows, [ "win32k" ] -> ()
9797+ | Windows, l ->
9898+ Fmt.failwith "%a/%a invalid os/os.features pair" OS.pp t.os
9999+ Fmt.(Dump.list string)
100100+ l
101101+ | _ -> ()
102102+ in
103103+ let () =
104104+ match (t.architecture, t.variant) with
105105+ | Arm, Some V6 | Arm, Some V7 | Arm, Some V8 | Arm64, Some V8 -> ()
106106+ | Arm, _ | Arm64, _ | _, Some _ -> err_arch_variant t
107107+ | _ -> ()
108108+ in
109109+ let () =
110110+ match (t.os, t.architecture) with
111111+ | Aix, Ppc64
112112+ | Android, X386
113113+ | Android, Xamd64
114114+ | Android, Arm
115115+ | Android, Arm64
116116+ | Darwin, Xamd64
117117+ | Darwin, Arm64
118118+ | Dragonfly, Xamd64
119119+ | Freebsd, X386
120120+ | Freebsd, Xamd64
121121+ | Freebsd, Arm
122122+ | Illumos, Xamd64
123123+ | Ios, Arm64
124124+ | Js, Wasm
125125+ | Linux, X386
126126+ | Linux, Xamd64
127127+ | Linux, Arm
128128+ | Linux, Arm64
129129+ | Linux, Loong64
130130+ | Linux, Mips
131131+ | Linux, Mipsle
132132+ | Linux, Mips64
133133+ | Linux, Mips64le
134134+ | Linux, Ppc64
135135+ | Linux, Ppc64le
136136+ | Linux, Riscv64
137137+ | Linux, S390x
138138+ | Netbsd, X386
139139+ | Netbsd, Xamd64
140140+ | Netbsd, Arm
141141+ | Openbsd, X386
142142+ | Openbsd, Xamd64
143143+ | Openbsd, Arm
144144+ | Openbsd, Arm64
145145+ | Plan9, X386
146146+ | Plan9, Xamd64
147147+ | Plan9, Arm
148148+ | Solaris, Xamd64
149149+ | Wasip1, Wasm
150150+ | Windows, X386
151151+ | Windows, Xamd64
152152+ | Windows, Arm
153153+ | Windows, Arm64 ->
154154+ ()
155155+ | _, _ -> err_os_arch t
156156+ in
157157+ let () =
158158+ match t.features with
159159+ | [] -> ()
160160+ | _ ->
161161+ failwith
162162+ "platform: features is reserved for future versions of the \
163163+ specification"
164164+ in
165165+ ()
+20
src/spec/platform.mli
···11+type t
22+33+val jsont : t Jsont.t
44+55+val v :
66+ ?os_version:string ->
77+ ?os_features:string list ->
88+ ?variant:Arch.variant ->
99+ Arch.t ->
1010+ OS.t ->
1111+ t
1212+1313+val unknown : t
1414+val pp : t Fmt.t
1515+val dump : t Fmt.t
1616+val to_string : t -> string
1717+val of_string : string -> (t, [ `Msg of string ]) result
1818+val arch : t -> Arch.t
1919+val os : t -> OS.t
2020+val check : t -> unit
+69
src/util.ml
···11+let guess_manifest (v : Oci_spec.Manifest.t) =
22+ let open Oci_spec in
33+ let arch = Osrelease.Arch.v () in
44+ let os = Osrelease.OS.v () in
55+ match v with
66+ | `Docker_manifest d -> (
77+ let platform = Manifest.Docker.config d |> Descriptor.platform in
88+ match platform with
99+ | None -> None
1010+ | Some platform ->
1111+ let m_os =
1212+ Osrelease.OS.of_string (Platform.os platform |> OS.to_string)
1313+ in
1414+ let m_arch =
1515+ Osrelease.Arch.of_string (Platform.arch platform |> Arch.to_string)
1616+ in
1717+ if arch = m_arch && os = m_os then Some (Manifest.Docker.config d)
1818+ else None)
1919+ | `OCI_manifest m -> (
2020+ let platform = Manifest.OCI.config m |> Descriptor.platform in
2121+ match platform with
2222+ | None -> None
2323+ | Some platform ->
2424+ let m_os =
2525+ Osrelease.OS.of_string (Platform.os platform |> OS.to_string)
2626+ in
2727+ let m_arch =
2828+ Osrelease.Arch.of_string (Platform.arch platform |> Arch.to_string)
2929+ in
3030+ if arch = m_arch && os = m_os then Some (Manifest.OCI.config m)
3131+ else None)
3232+ | `Docker_manifest_list l ->
3333+ let manifests = Manifest_list.manifests l in
3434+ let manifest =
3535+ Stdlib.List.find_opt
3636+ (fun m ->
3737+ match Descriptor.platform m with
3838+ | None -> false
3939+ | Some (platform : Platform.t) ->
4040+ let m_os =
4141+ Osrelease.OS.of_string (Platform.os platform |> OS.to_string)
4242+ in
4343+ let m_arch =
4444+ Osrelease.Arch.of_string
4545+ (Platform.arch platform |> Arch.to_string)
4646+ in
4747+ arch = m_arch && os = m_os)
4848+ manifests
4949+ in
5050+ manifest
5151+ | `OCI_index l ->
5252+ let manifests = Index.manifests l in
5353+ let manifest =
5454+ Stdlib.List.find_opt
5555+ (fun m ->
5656+ match Descriptor.platform m with
5757+ | None -> false
5858+ | Some (platform : Platform.t) ->
5959+ let m_os =
6060+ Osrelease.OS.of_string (Platform.os platform |> OS.to_string)
6161+ in
6262+ let m_arch =
6363+ Osrelease.Arch.of_string
6464+ (Platform.arch platform |> Arch.to_string)
6565+ in
6666+ arch = m_arch && os = m_os)
6767+ manifests
6868+ in
6969+ manifest
+3
src/util.mli
···11+val guess_manifest : Oci_spec.Manifest.t -> Oci_spec.Descriptor.t option
22+(** [guess_manifest manifest] will try to use operating system information (e.g.
33+ architecture) of the host to guess a distinct manifest to use. *)