use crate::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QcowHeaderExtensionKind {
End,
FeatureNameTable,
BackingFileFormat,
ExternalDataPath,
FullDiskEncryption,
Bitmaps,
Unknown(u32),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QcowHeaderExtension {
pub kind: QcowHeaderExtensionKind,
pub data: Vec<u8>,
}
impl QcowHeaderExtension {
pub fn parse_many(data: &[u8]) -> Result<Vec<Self>> {
let mut cursor = 0usize;
let mut extensions = Vec::new();
while cursor < data.len() {
let header = data
.get(cursor..cursor + 8)
.ok_or_else(|| Error::invalid_format("qcow header extension header is truncated"))?;
let kind_raw = u32::from_be_bytes([header[0], header[1], header[2], header[3]]);
let data_len = usize::try_from(u32::from_be_bytes([
header[4], header[5], header[6], header[7],
]))
.map_err(|_| Error::invalid_range("qcow header extension length is too large"))?;
cursor += 8;
if kind_raw == 0 {
extensions.push(Self {
kind: QcowHeaderExtensionKind::End,
data: Vec::new(),
});
break;
}
let payload = data
.get(cursor..cursor + data_len)
.ok_or_else(|| Error::invalid_format("qcow header extension payload is truncated"))?;
extensions.push(Self {
kind: kind_from_u32(kind_raw),
data: payload.to_vec(),
});
cursor = cursor
.checked_add(data_len)
.ok_or_else(|| Error::invalid_range("qcow header extension cursor overflow"))?;
let padding = (8 - (cursor % 8)) % 8;
cursor = cursor
.checked_add(padding)
.ok_or_else(|| Error::invalid_range("qcow header extension alignment overflow"))?;
}
Ok(extensions)
}
pub fn utf8_string(&self) -> Result<Option<String>> {
match self.kind {
QcowHeaderExtensionKind::BackingFileFormat | QcowHeaderExtensionKind::ExternalDataPath => {
let mut data = self.data.clone();
while data.last() == Some(&0) {
data.pop();
}
let string = String::from_utf8(data)
.map_err(|_| Error::invalid_format("qcow header extension string is not valid UTF-8"))?;
Ok(Some(string))
}
_ => Ok(None),
}
}
}
fn kind_from_u32(value: u32) -> QcowHeaderExtensionKind {
match value {
0 => QcowHeaderExtensionKind::End,
0x6803F857 => QcowHeaderExtensionKind::FeatureNameTable,
0xE2792ACA => QcowHeaderExtensionKind::BackingFileFormat,
0x44415441 => QcowHeaderExtensionKind::ExternalDataPath,
0x0537BE77 => QcowHeaderExtensionKind::FullDiskEncryption,
0x23852875 => QcowHeaderExtensionKind::Bitmaps,
other => QcowHeaderExtensionKind::Unknown(other),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_backing_format_extension() {
let data = [
0xE2, 0x79, 0x2A, 0xCA, 0x00, 0x00, 0x00, 0x04, b'r', b'a', b'w', 0x00, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
];
let extensions = QcowHeaderExtension::parse_many(&data).unwrap();
assert_eq!(extensions.len(), 2);
assert_eq!(
extensions[0].kind,
QcowHeaderExtensionKind::BackingFileFormat
);
assert_eq!(extensions[0].utf8_string().unwrap().as_deref(), Some("raw"));
}
}