My own corner of monopam
README.md

xml #

Declarative XML codecs for OCaml, with streaming input through bytesrw and errors that name the path to the failing node.

Code that reads XML usually walks a generic tree, pattern-matches on tags and converts strings by hand, and then writes the same mapping again in reverse to produce XML. This library describes the mapping once, as a codec value, and uses it in both directions. It follows the finally tagged design of Daniel Bünzli's jsont, described in An alphabet for your data soups: the El builder composes attributes, child elements and text into an element codec, the same way jsont composes JSON objects from members.

Input is read from a string or a bytesrw reader, and UTF-8 is validated as it arrives. A decoding error is a loc error that carries the line, the column and the path from the root to the node that failed.

Installation #

$ opam install nox-xml

If opam cannot find the package, it has not reached the public opam-repository yet. Add the overlay repository first:

$ opam repo add samoht https://tangled.org/gazagnaire.org/opam-overlay.git
$ opam update
$ opam install nox-xml

Usage #

type person = { name : string; age : int; email : string option }

let person : person Xml.codec =
  let open Xml.Codec in
  element "person"
    El.(
      obj (fun name age email -> { name; age; email })
      |> attr "name" Attr.string ~enc:(fun p -> p.name)
      |> attr "age" Attr.int ~enc:(fun p -> p.age)
      |> child_opt "email" string ~enc:(fun p -> p.email)
      |> finish)

(* Decode: error is a formatted string with line/column and path. *)
let alice =
  match Xml.of_string person "<person name=\"Alice\" age=\"30\"/>" with
  | Ok p -> Printf.printf "Name: %s, Age: %d\n" p.name p.age
  | Error _ -> failwith "invalid XML"

(* Encode. Raises [Xml.Invalid_xml_char] if the input string contains
   a codepoint that XML 1.0 forbids (NUL, lone surrogates, ...). *)
let xml =
  Xml.to_string ~indent:2 person { name = "Bob"; age = 25; email = None }

The error returned by Xml.of_string is the structured Xml.Error.t, which a caller can inspect for diagnostics or format with Xml.Error.pp. The source-location modules are re-exported as Xml.Loc, Xml.Meta and Xml.Path:

let report input =
  match Xml.of_string person input with
  | Ok _ -> ()
  | Error _ -> Format.eprintf "invalid XML@."

API overview #

  • Xml.Codec holds the combinators: the text codecs string, int, float and bool; element to name an element; Attr for attribute values; El to build an element from its attributes, children and text (obj, attr, attr_opt, child, child_opt, children, text, finish); and map, const, option, list, fix, child, attr and nth.
  • Xml.of_string and Xml.of_reader decode, returning ('a, Xml.Error.t) result; the _exn variants raise instead.
  • Xml.to_string and Xml.to_writer encode. They raise Xml.Invalid_xml_char on malformed UTF-8 or a code point XML 1.0 forbids.
  • Xml.decode and Xml.encode convert between a codec and the tree Xml.Value.t, which has its own parser, printer and queries.
  • Xml.Cursor is a zipper over that tree, with XPath-like pointers and the update_child and delete_child codecs for edits.
  • Xml.Sort names the node categories used in error messages.

Every decoding function takes ?max_depth (default 100) and ?max_nodes (default ten million), which bound what an untrusted document can make the decoder allocate. An entity expansion bomb cannot reach it at all. An XML document can declare entities that expand to other entities: ten levels, each expanding to ten copies of the level below, turn a few hundred bytes into 10^9 copies of a string (the "billion laughs" attack, CVE-2003-1564), and a decoder that expands them without a bound exhausts memory. This decoder skips the DOCTYPE, where such entities are declared, and refuses a reference to any entity other than the five predefined ones and character references, so nothing it reads expands.

The full signatures are in xml.mli.

References #

  • jsont is the JSON codec library whose design this library follows.
  • bytesrw provides the byte stream readers and writers.
  • loc provides the source locations and the error type.

License #

ISC. See LICENSE.md.