blob: 454e08b3408e249262a31bb877791bcd744b34e6 [file]
//! What a caller asks of the API, in plain Rust: the containers, processes, volumes and
//! images. Platform independent, so a caller builds and tests these anywhere and only the
//! calls that act on them are Windows only.
use std::fmt;
/// How a container reaches the network.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Networking {
/// Loopback only.
None,
/// The session's bridge, and through it whatever the session VM reaches.
Bridged,
}
/// A host directory mounted into a container.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Bind {
/// The directory as Windows names it: a drive path, or a `\\wsl.localhost` UNC name.
pub host: String,
/// The absolute path inside the container.
pub container: String,
/// Mounted read-only.
pub read_only: bool,
}
/// A session volume mounted into a container.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Mount {
/// The volume's name in the session.
pub volume: String,
/// The absolute path inside the container.
pub container: String,
/// Mounted read-only.
pub read_only: bool,
}
/// A process: what runs, with what environment, where.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ProcessSpec {
/// The command line, program first.
pub argv: Vec<String>,
/// The environment, complete: nothing is inherited from the image beyond what the
/// runtime itself adds.
pub env: Vec<(String, String)>,
/// The working directory, or the image's.
pub cwd: Option<String>,
}
/// A container: its image, name, network, mounts and first process.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ContainerSpec {
/// The image, by the name the session store knows it by.
pub image: String,
/// A name, or one the runtime picks.
pub name: Option<String>,
/// How it reaches the network.
pub networking: Networking,
/// Host directories.
pub binds: Vec<Bind>,
/// Session volumes.
pub mounts: Vec<Mount>,
/// The init process; the image's command when `argv` is empty.
pub init: ProcessSpec,
/// Removed by the runtime when its init process exits.
pub auto_remove: bool,
}
/// An image in a session's store.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ImageInfo {
/// The name it is stored under.
pub name: String,
/// The image id: the sha256 of its configuration, as lowercase hex.
pub id: String,
/// Its size.
pub size_bytes: i64,
}
impl ImageInfo {
/// Whether this is the image `reference` names: by its stored name, or by a name that
/// differs only in the registry the runtime fills in for a short name.
#[must_use]
pub fn answers_to(&self, reference: &str) -> bool {
let short = |s: &str| {
let s = s.strip_prefix("docker.io/").unwrap_or(s);
let s = s.strip_prefix("library/").unwrap_or(s);
if s.rsplit('/').next().is_some_and(|last| last.contains(':')) || s.contains('@') {
s.to_string()
} else {
format!("{s}:latest")
}
};
short(&self.name) == short(reference)
}
}
/// An id as `wslc image inspect` prints it, `sha256:<hex>`, or the bare hex, reduced to
/// the hex, lowercase.
#[must_use]
pub fn bare_id(id: &str) -> String {
id.trim()
.strip_prefix("sha256:")
.unwrap_or(id.trim())
.to_ascii_lowercase()
}
/// A failed call: which, its HRESULT, and the message the API gave when it gave one.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Error {
/// The API function, or what failed before one was called.
pub call: &'static str,
/// The HRESULT, or 0 where none was returned.
pub hr: u32,
/// The API's message.
pub message: String,
}
impl Error {
/// A failure that is not an HRESULT: a missing DLL, a string with a NUL in it.
#[must_use]
pub fn other(call: &'static str, message: impl Into<String>) -> Self {
Self {
call,
hr: 0,
message: message.into(),
}
}
/// The header's name for this HRESULT, when it has one.
#[must_use]
pub fn name(&self) -> Option<&'static str> {
hresult_name(self.hr)
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.call)?;
if self.hr != 0 {
write!(f, ": 0x{:08x}", self.hr)?;
if let Some(n) = self.name() {
write!(f, " {n}")?;
}
}
if !self.message.is_empty() {
write!(f, ": {}", self.message.trim())?;
}
Ok(())
}
}
impl std::error::Error for Error {}
/// The WSLC error codes `wslcsdk.h` defines, and the common Windows ones the API returns.
#[must_use]
pub fn hresult_name(hr: u32) -> Option<&'static str> {
const WSLC: [&str; 19] = [
"WSLC_E_IMAGE_NOT_FOUND",
"WSLC_E_CONTAINER_PREFIX_AMBIGUOUS",
"WSLC_E_CONTAINER_NOT_FOUND",
"WSLC_E_VOLUME_NOT_FOUND",
"WSLC_E_CONTAINER_NOT_RUNNING",
"WSLC_E_CONTAINER_IS_RUNNING",
"WSLC_E_SESSION_RESERVED",
"WSLC_E_INVALID_SESSION_NAME",
"WSLC_E_NETWORK_NOT_FOUND",
"WSLC_E_WU_SEARCH_FAILED",
"WSLC_E_SDK_UPDATE_NEEDED",
"WSLC_E_CONTAINER_DISABLED",
"WSLC_E_REGISTRY_BLOCKED_BY_POLICY",
"WSLC_E_VOLUME_NOT_AVAILABLE",
"WSLC_E_SESSION_NOT_FOUND",
"WSLC_E_VM_NOT_RUNNING",
"WSLC_E_EVENTS_LOST",
"WSLC_E_EVENT_STREAM_FINISHED",
"WSLC_E_CONTAINER_DELETED",
];
// MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 0x0600 + n)
if let Some(name) = hr
.checked_sub(0x8004_0601)
.and_then(|n| WSLC.get(n as usize))
{
return Some(name);
}
match hr {
0x8007_0002 => Some("ERROR_FILE_NOT_FOUND"),
0x8007_0005 => Some("E_ACCESSDENIED"),
0x8007_0020 => Some("ERROR_SHARING_VIOLATION"),
0x8007_00b7 => Some("ERROR_ALREADY_EXISTS"),
0x8007_0057 => Some("E_INVALIDARG"),
0x8000_4001 => Some("E_NOTIMPL"),
0x8000_4005 => Some("E_FAIL"),
0x8000_ffff => Some("E_UNEXPECTED"),
0x8004_01f0 => Some("CO_E_NOTINITIALIZED"),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_header_error_codes_are_named() {
assert_eq!(hresult_name(0x8004_0601), Some("WSLC_E_IMAGE_NOT_FOUND"));
assert_eq!(hresult_name(0x8004_0607), Some("WSLC_E_SESSION_RESERVED"));
assert_eq!(hresult_name(0x8004_0613), Some("WSLC_E_CONTAINER_DELETED"));
assert_eq!(hresult_name(0x8004_0614), None);
assert_eq!(hresult_name(0x8004_0600), None);
assert_eq!(hresult_name(0x8007_0057), Some("E_INVALIDARG"));
let e = Error {
call: "WslcCreateContainer",
hr: 0x8004_0601,
message: "no such image\r\n".into(),
};
assert_eq!(
e.to_string(),
"WslcCreateContainer: 0x80040601 WSLC_E_IMAGE_NOT_FOUND: no such image"
);
assert_eq!(
Error::other("wslcsdk.dll", "not found").to_string(),
"wslcsdk.dll: not found"
);
}
#[test]
fn an_image_answers_to_its_name_with_or_without_the_default_registry() {
let i = ImageInfo {
name: "durable-memory/harness:dev".into(),
id: "1d37".into(),
size_bytes: 1,
};
assert!(i.answers_to("durable-memory/harness:dev"));
assert!(i.answers_to("docker.io/durable-memory/harness:dev"));
assert!(!i.answers_to("durable-memory/harness:other"));
assert!(!i.answers_to("durable-memory/harness"));
let latest = ImageInfo {
name: "docker.io/library/python:latest".into(),
..i.clone()
};
assert!(latest.answers_to("python"));
assert!(!latest.answers_to("python:3"));
let registry = ImageInfo {
name: "localhost:5000/x".into(),
..i
};
assert!(
registry.answers_to("localhost:5000/x:latest"),
"a port is not a tag"
);
}
#[test]
fn an_id_is_compared_as_bare_lowercase_hex() {
assert_eq!(bare_id("sha256:1D37ab\n"), "1d37ab");
assert_eq!(bare_id("1d37ab"), "1d37ab");
}
}