diff --git a/tools/rust/xenbindgen/src/main.rs b/tools/rust/xenbindgen/src/main.rs
index 08fcf8fe4da6..497cf39d3bbd 100644
--- a/tools/rust/xenbindgen/src/main.rs
+++ b/tools/rust/xenbindgen/src/main.rs
@@ -2,14 +2,22 @@
 //! crafted TOML files. The format of these files follows the following
 //! rules.
 
+mod spec;
+
+use std::path::PathBuf;
+
 use clap::Parser;
 use env_logger::Env;
-use log::info;
+use log::{error, info};
 
 /// A CLI tool to generate struct definitions in several languages.
 #[derive(Parser, Debug)]
 #[command(version, about)]
-struct Cli;
+struct Cli {
+    /// Path to the input directory containing the hypercall specification
+    #[arg(short, long)]
+    indir: PathBuf,
+}
 
 fn main() {
     env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
@@ -17,5 +25,19 @@ fn main() {
     let cli = Cli::parse();
     info!("args: {:?}", cli);
 
-    todo!("read spec files and generate output files");
+    let _specification = match spec::Spec::new(&cli.indir) {
+        Ok(x) => x,
+        Err(spec::Error::Toml(x)) => {
+            error!("TOML parsing error:");
+            error!("{x:#?}");
+            std::process::exit(1);
+        }
+        Err(spec::Error::Io(x)) => {
+            error!("IO error:");
+            error!("{x:#?}");
+            std::process::exit(1);
+        }
+    };
+
+    todo!("generate output files");
 }
diff --git a/tools/rust/xenbindgen/src/spec.rs b/tools/rust/xenbindgen/src/spec.rs
new file mode 100644
index 000000000000..e69f7c78dc7a
--- /dev/null
+++ b/tools/rust/xenbindgen/src/spec.rs
@@ -0,0 +1,96 @@
+//! Specification descriptions
+//!
+//! The TOML files are not parsed by hand. This module provides a sort of
+//! schema for the TOML descriptions, in the sense that `serde` itself ensures
+//! every field is deserialised into its equivalent Rust structure or the
+//! deserialization procedure fails.
+//!
+//! If the specification is clearly invalid (i.e: missing fields) it'll scream
+//! in a rather obvious way.
+//!
+//! A special case is the `typ` field in the specifications is meant to have
+//! the format present in the specifications under `extra`. This allows `serde`
+//! to properly decode the type and match it with a variant of the [`Typ`] type
+//! with the payload landing in the payload of the variant itself.
+
+use std::{fs::read_to_string, path::Path};
+
+use log::{debug, info};
+
+/// A language-agnostic specification.
+#[derive(Debug, serde::Deserialize)]
+struct InFileDef;
+
+/// Description of an abstract output (i.e: `.rs`, `.h`, etc).
+///
+/// Contains every element of the ABI that needs representing.
+#[derive(Debug)]
+pub struct OutFileDef {
+    /// The name of the output file, without the final extension.
+    pub name: String,
+}
+
+impl OutFileDef {
+    /// Creates a new _output_ file description. Each [`OutFileDef`] is
+    /// associated with a number of [`InFileDef`] and holds the merged
+    /// contents described in all of them.
+    ///
+    /// # Errors
+    /// Fails if the TOML is invalid or on IO error.
+    pub fn new(name: String, dir: &Path) -> Result<Self, Error> {
+        info!("Reading {dir:?} to generate an output file");
+
+        let mut ret = Self { name };
+
+        for entry in from_ioerr(dir.read_dir())? {
+            let path = from_ioerr(entry)?.path();
+            debug!("Reading {:?} to generate outfile={}", path, ret.name);
+            let toml_str = from_ioerr(read_to_string(path))?;
+            let filedef: InFileDef = toml::from_str(&toml_str).map_err(Error::Toml)?;
+        }
+
+        Ok(ret)
+    }
+}
+
+/// Internal error type for every error spec parsing could encounter
+#[derive(Debug)]
+pub enum Error {
+    /// Wrapper around IO errors
+    Io(std::io::Error),
+    /// Wrapper around deserialization errors
+    Toml(toml::de::Error),
+}
+
+/// Maps an [`std::io::Error`] onto a [`Error`] type for easier propagation
+fn from_ioerr<T>(t: std::io::Result<T>) -> Result<T, Error> {
+    t.map_err(Error::Io)
+}
+
+/// Object containing the abstract definitions of all output files.
+///
+/// See [`OutFileDef`] to details on the specification contents of each output.
+#[derive(Debug)]
+pub struct Spec(pub Vec<OutFileDef>);
+
+impl Spec {
+    /// Creates a new abstract specification from a top-level directory full
+    /// of specification files. This is used later to aggregate all the content
+    /// and generate the appropriate language outputs.
+    ///
+    /// # Errors
+    /// Fails on IO errors.
+    pub fn new(root: &Path) -> Result<Self, Error> {
+        info!("Reading {root:?} as top-level directory");
+
+        let mut ret = Self(Vec::new());
+        for outfile in from_ioerr(root.read_dir())? {
+            // Each folder in the root defines a single output file
+            let outfile = from_ioerr(outfile)?;
+            let name = outfile.file_name().to_string_lossy().to_string();
+            ret.0.push(OutFileDef::new(name, &outfile.path())?);
+        }
+
+        Ok(ret)
+    }
+}
