use pest::Parser;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use std::str::FromStr;
#[derive(Debug, Fail)]
#[allow(clippy::large_enum_variant)]
pub enum ManifestError {
#[fail(display = "JSON Error: {:?}", _0)]
JsonError(serde_json::Error),
#[fail(display = "Invalid Schema Version: {}", _0)]
InvalidSchemaVersion(u64),
#[fail(display = "Invalid (unknown) Media Type: {}", _0)]
InvalidMediaType(String),
#[fail(display = "Parsing digest failed: '{}' ({:?})", _0, _1)]
DigestParseFailed(String, #[cause] pest::error::Error<Rule>),
#[fail(display = "Invalid digest algorithm: {}", _0)]
InvalidDigestAlgorithm(String),
}
#[derive(Debug, Deserialize)]
struct ManifestSchemaOnlyV2 {
#[serde(rename = "schemaVersion")]
schema: u64,
}
impl ManifestSchemaOnlyV2 {
pub fn schema(&self) -> u64 {
self.schema
}
}
#[derive(Debug, Deserialize)]
struct ManifestMediaTypeOnlyV2_2 {
#[serde(rename = "mediaType")]
media_type: String,
}
impl ManifestMediaTypeOnlyV2_2 {
pub fn media_type(&self) -> &str {
&self.media_type
}
}
#[derive(Debug)]
pub enum ManifestV2 {
Schema1(ManifestV2_1),
Schema2(ManifestV2_2),
Schema2List(ManifestListV2_2),
}
impl FromStr for ManifestV2 {
type Err = ManifestError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match probe_manifest_v2_schema(s)? {
ManifestV2Schema::Schema1 => serde_json::from_str(s).map(ManifestV2::Schema1),
ManifestV2Schema::Schema2 => serde_json::from_str(s).map(ManifestV2::Schema2),
ManifestV2Schema::Schema2List => serde_json::from_str(s).map(ManifestV2::Schema2List),
}
.map_err(ManifestError::JsonError)
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum ManifestV2Schema {
Schema1,
Schema2,
Schema2List,
}
impl From<ManifestV2> for ManifestV2Schema {
fn from(manifest: ManifestV2) -> Self {
match manifest {
ManifestV2::Schema1(_) => ManifestV2Schema::Schema1,
ManifestV2::Schema2(_) => ManifestV2Schema::Schema2,
ManifestV2::Schema2List(_) => ManifestV2Schema::Schema2List,
}
}
}
impl From<&ManifestV2> for ManifestV2Schema {
fn from(manifest: &ManifestV2) -> Self {
match manifest {
ManifestV2::Schema1(_) => ManifestV2Schema::Schema1,
ManifestV2::Schema2(_) => ManifestV2Schema::Schema2,
ManifestV2::Schema2List(_) => ManifestV2Schema::Schema2List,
}
}
}
pub fn probe_manifest_v2_schema(data: &str) -> Result<ManifestV2Schema, ManifestError> {
let manifest: ManifestSchemaOnlyV2 =
serde_json::from_str(data).map_err(ManifestError::JsonError)?;
match manifest.schema() {
1 => return Ok(ManifestV2Schema::Schema1),
2 => {}
schema => return Err(ManifestError::InvalidSchemaVersion(schema)),
};
let manifest: ManifestMediaTypeOnlyV2_2 =
serde_json::from_str(data).map_err(ManifestError::JsonError)?;
let media_type = manifest.media_type();
#[allow(clippy::or_fun_call)]
let media_type_split = media_type
.split('+')
.next()
.ok_or(ManifestError::InvalidMediaType(media_type.into()))?;
match media_type_split {
"application/vnd.oci.distribution.manifest.v2" => Ok(ManifestV2Schema::Schema2),
"application/vnd.oci.distribution.manifest.list.v2" => Ok(ManifestV2Schema::Schema2List),
"application/vnd.docker.distribution.manifest.v2" => Ok(ManifestV2Schema::Schema2),
"application/vnd.docker.distribution.manifest.list.v2" => Ok(ManifestV2Schema::Schema2List),
_ => Err(ManifestError::InvalidMediaType(media_type.into())),
}
}
#[derive(Parser)]
#[grammar = "image/digest.pest"]
struct DigestParser;
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
pub struct Digest {
pub algorithm: DigestAlgorithm,
pub hex: String,
}
impl std::fmt::Display for Digest {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}:{}", self.algorithm, self.hex)
}
}
impl std::str::FromStr for Digest {
type Err = ManifestError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut digest = DigestParser::parse(Rule::digest, s)
.map_err(|e| ManifestError::DigestParseFailed(s.into(), e))?
.next()
.unwrap() .into_inner()
.map(|t| t.as_str().to_owned());
let algorithm: DigestAlgorithm = digest.next().unwrap().parse()?;
let hex = digest.next().unwrap();
Ok(Self { algorithm, hex })
}
}
impl<'de> Deserialize<'de> for Digest {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer)?
.parse()
.map_err(de::Error::custom)
}
}
impl Serialize for Digest {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(self)
}
}
#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
pub enum DigestAlgorithm {
Sha256,
}
impl std::fmt::Display for DigestAlgorithm {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
DigestAlgorithm::Sha256 => write!(f, "sha256"),
}
}
}
impl std::str::FromStr for DigestAlgorithm {
type Err = ManifestError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"sha256" => Ok(DigestAlgorithm::Sha256),
other => Err(ManifestError::InvalidDigestAlgorithm(other.into())),
}
}
}
impl<'de> Deserialize<'de> for DigestAlgorithm {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer)?
.parse()
.map_err(de::Error::custom)
}
}
impl Serialize for DigestAlgorithm {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(self)
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct FsLayerV2_1 {
#[serde(rename = "blobSum")]
inner: Digest,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct V1Compatibility {
#[serde(rename = "v1Compatibility")]
inner: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ManifestV2_1 {
#[serde(rename = "schemaVersion")]
schema: u64,
name: String,
tag: String,
architecture: String,
#[serde(rename = "fsLayers")]
layers: Vec<FsLayerV2_1>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ConfigV2_2 {
#[serde(rename = "mediaType")]
media_type: String,
size: usize,
digest: Digest,
}
impl ConfigV2_2 {
pub fn digest(&self) -> &Digest {
&self.digest
}
}
#[derive(Debug, Deserialize, Serialize, PartialEq)]
pub struct LayerV2_2 {
#[serde(rename = "mediaType")]
media_type: String,
size: usize,
digest: Digest,
urls: Option<Vec<String>>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ManifestV2_2 {
#[serde(rename = "schemaVersion")]
pub schema: u64,
#[serde(rename = "mediaType")]
pub media_type: String,
#[serde(rename = "config")]
pub config: ConfigV2_2,
pub layers: Vec<LayerV2_2>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ManifestPlatformV2_2 {
architecture: String,
os: String,
#[serde(rename = "os.version")]
osversion: Option<String>,
#[serde(rename = "os.features")]
osfeatures: Option<Vec<String>>,
variant: Option<String>,
features: Option<Vec<String>>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ManifestListEntryV2_2 {
#[serde(rename = "mediaType")]
media_type: String,
size: usize,
digest: Digest,
platform: ManifestPlatformV2_2,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ManifestListV2_2 {
#[serde(rename = "schemaVersion")]
pub schema: u64,
media_type: String,
manifests: Vec<ManifestListEntryV2_2>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json;
#[test]
fn test_manifest_v1() {
let test_data = include_str!("test/manifest-v2-1.test.json");
let manifest: ManifestV2_1 =
serde_json::from_str(test_data).expect("Could not deserialize manifest");
assert_eq!(manifest.schema, 1);
assert_eq!(manifest.name, "hello-world");
assert_eq!(manifest.tag, "latest");
assert_eq!(manifest.architecture, "amd64");
assert_eq!(manifest.layers.len(), 4);
}
#[test]
fn test_manifest_v2() {
let test_data = include_str!("test/manifest-v2-2.test.json");
let manifest: ManifestV2_2 =
serde_json::from_str(test_data).expect("Could not deserialize manifest");
assert_eq!(manifest.schema, 2);
assert_eq!(
manifest.media_type,
"application/vnd.docker.distribution.manifest.v2+json"
);
assert_eq!(
manifest.config.media_type,
"application/vnd.docker.container.image.v1+json"
);
assert_eq!(manifest.config.size, 7023);
assert_eq!(manifest.config.digest.algorithm, DigestAlgorithm::Sha256);
assert_eq!(
manifest.config.digest.hex,
"b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
);
assert_eq!(manifest.layers.len(), 3);
assert_eq!(
manifest.layers[0],
LayerV2_2 {
media_type: "application/vnd.docker.image.rootfs.diff.tar.gzip".into(),
size: 32654,
digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"
.parse()
.expect("Could not parse reference digest"),
urls: None,
}
);
assert_eq!(
manifest.layers[1],
LayerV2_2 {
media_type: "application/vnd.docker.image.rootfs.diff.tar.gzip".into(),
size: 16724,
digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b"
.parse()
.expect("Could not parse reference digest"),
urls: None,
}
);
assert_eq!(
manifest.layers[2],
LayerV2_2 {
media_type: "application/vnd.docker.image.rootfs.diff.tar.gzip".into(),
size: 73109,
digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736"
.parse()
.expect("Could not parse reference digest"),
urls: None,
}
);
}
#[test]
fn test_manifest_list_v2() {
let test_data = include_str!("test/manifest-list-v2-2.test.json");
let manifest_list: ManifestListV2_2 =
serde_json::from_str(test_data).expect("Could not deserialize manifest list");
assert_eq!(manifest_list.schema, 2);
assert_eq!(
manifest_list.media_type,
"application/vnd.docker.distribution.manifest.list.v2+json"
);
assert_eq!(manifest_list.manifests.len(), 2);
}
#[test]
fn test_manifest_schemaonly_schema1() {
let test_data = include_str!("test/manifest-v2-1.test.json");
let manifest: ManifestSchemaOnlyV2 =
serde_json::from_str(test_data).expect("Could not deserialize manifest");
assert_eq!(manifest.schema(), 1);
}
#[test]
fn test_manifest_schemaonly_schema2() {
let test_data = include_str!("test/manifest-v2-2.test.json");
let manifest: ManifestSchemaOnlyV2 =
serde_json::from_str(test_data).expect("Could not deserialize manifest");
assert_eq!(manifest.schema(), 2);
}
#[test]
fn test_manifest_schemaonly_schema2_list() {
let test_data = include_str!("test/manifest-list-v2-2.test.json");
let manifest: ManifestSchemaOnlyV2 =
serde_json::from_str(test_data).expect("Could not deserialize manifest");
assert_eq!(manifest.schema(), 2);
}
#[test]
fn test_manifest_mediatypeonly_schema2() {
let test_data = include_str!("test/manifest-v2-2.test.json");
let manifest: ManifestMediaTypeOnlyV2_2 =
serde_json::from_str(test_data).expect("Could not deserialize manifest");
assert_eq!(
manifest.media_type(),
"application/vnd.docker.distribution.manifest.v2+json"
);
}
#[test]
fn test_manifest_mediatypeonly_schema2_list() {
let test_data = include_str!("test/manifest-list-v2-2.test.json");
let manifest: ManifestMediaTypeOnlyV2_2 =
serde_json::from_str(test_data).expect("Could not deserialize manifest");
assert_eq!(
manifest.media_type(),
"application/vnd.docker.distribution.manifest.list.v2+json"
);
}
#[test]
fn test_probe_manifest_schema1() {
let test_data = include_str!("test/manifest-v2-1.test.json");
let schema = probe_manifest_v2_schema(test_data).expect("could not probe manifest schema");
assert_eq!(schema, ManifestV2Schema::Schema1);
}
#[test]
fn test_probe_manifest_schema2() {
let test_data = include_str!("test/manifest-v2-2.test.json");
let schema = probe_manifest_v2_schema(test_data).expect("could not probe manifest schema");
assert_eq!(schema, ManifestV2Schema::Schema2);
}
#[test]
fn test_probe_manifest_schema2_list() {
let test_data = include_str!("test/manifest-list-v2-2.test.json");
let schema = probe_manifest_v2_schema(test_data).expect("could not probe manifest schema");
assert_eq!(schema, ManifestV2Schema::Schema2List);
}
#[test]
fn test_parse_manifest_v2() {
let test_data = include_str!("test/manifest-v2-1.test.json");
let manifest: ManifestV2 = test_data
.parse()
.expect("Could not parse manifest schema 1");
assert_eq!(ManifestV2Schema::from(manifest), ManifestV2Schema::Schema1);
let test_data = include_str!("test/manifest-v2-2.test.json");
let manifest: ManifestV2 = test_data
.parse()
.expect("Could not parse manifest schema 2");
assert_eq!(ManifestV2Schema::from(manifest), ManifestV2Schema::Schema2);
let test_data = include_str!("test/manifest-list-v2-2.test.json");
let manifest: ManifestV2 = test_data
.parse()
.expect("Could not parse manifest schema 2 list");
assert_eq!(
ManifestV2Schema::from(manifest),
ManifestV2Schema::Schema2List
);
}
#[test]
fn test_parse_digest() {
let test_data = "sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b";
let digest: Digest = test_data.parse().expect("Could not parse digest");
assert_eq!(digest.algorithm, DigestAlgorithm::Sha256);
assert_eq!(
digest.hex,
"6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b"
);
assert_eq!(&digest.to_string(), test_data)
}
#[test]
fn test_parse_digest_fail() {
"foobar"
.parse::<Digest>()
.expect_err("parsing of string without : succeeded");
"a::deadbeef"
.parse::<Digest>()
.expect_err("digest with multiple : succeeded");
"sha256:xxxyyyzzz"
.parse::<Digest>()
.expect_err("parsing digest with non-hex string succeeded");
}
}