Skip to main content

bb_bmap_parser/
xml.rs

1use crate::bmap::{BmapBuilder, BmapBuilderError, HashValue};
2use quick_xml::de::{DeError, from_str};
3use serde::Deserialize;
4use thiserror::Error;
5
6/// Custom deserializer to first trim whitespace around text elements before converting.
7/// Should be unnecessary once https://github.com/tafia/quick-xml/issues/900 gets fixed
8fn deserialize_trimmed<'de, D, T>(deserializer: D) -> Result<T, D::Error>
9where
10    D: serde::Deserializer<'de>,
11    T: std::str::FromStr,
12    <T as std::str::FromStr>::Err: std::fmt::Display,
13{
14    let s = <&str>::deserialize(deserializer)?;
15    s.trim().parse().map_err(serde::de::Error::custom)
16}
17
18#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
19#[serde(rename_all = "lowercase")]
20#[non_exhaustive]
21pub enum HashType {
22    Sha256,
23}
24
25impl std::str::FromStr for HashType {
26    type Err = &'static str;
27
28    fn from_str(s: &str) -> Result<Self, Self::Err> {
29        match s {
30            "sha256" => Ok(Self::Sha256),
31            _ => Err("Unsupported"),
32        }
33    }
34}
35
36#[derive(Debug, Deserialize)]
37struct Range {
38    #[serde(rename = "@chksum", deserialize_with = "deserialize_trimmed")]
39    chksum: String,
40    #[serde(rename = "$value", deserialize_with = "deserialize_trimmed")]
41    range: String,
42}
43
44#[derive(Debug, Deserialize)]
45struct BlockMap {
46    #[serde(rename = "Range")]
47    ranges: Vec<Range>,
48}
49
50#[allow(dead_code)]
51#[derive(Debug, Deserialize)]
52struct Bmap {
53    #[serde(rename = "@version", deserialize_with = "deserialize_trimmed")]
54    version: String,
55    #[serde(rename = "ImageSize", deserialize_with = "deserialize_trimmed")]
56    image_size: u64,
57    #[serde(rename = "BlockSize", deserialize_with = "deserialize_trimmed")]
58    block_size: u64,
59    #[serde(rename = "BlocksCount", deserialize_with = "deserialize_trimmed")]
60    blocks_count: u64,
61    #[serde(rename = "MappedBlocksCount", deserialize_with = "deserialize_trimmed")]
62    mapped_blocks_count: u64,
63    #[serde(rename = "ChecksumType", deserialize_with = "deserialize_trimmed")]
64    checksum_type: HashType,
65    #[serde(rename = "BmapFileChecksum", deserialize_with = "deserialize_trimmed")]
66    bmap_file_checksum: String,
67    #[serde(rename = "BlockMap")]
68    block_map: BlockMap,
69}
70
71#[derive(Debug, Error)]
72pub enum XmlError {
73    #[error("Failed to parse bmap XML: {0}")]
74    XmlParsError(#[from] DeError),
75    #[error("Invalid bmap file: {0}")]
76    InvalidFIleError(#[from] BmapBuilderError),
77    #[error("Unknown checksum type: {0}")]
78    UnknownChecksumType(String),
79    #[error("Invalid checksum: {0}")]
80    InvalidChecksum(String),
81}
82
83const fn hexdigit_to_u8(c: u8) -> Option<u8> {
84    match c {
85        b'a'..=b'f' => Some(c - b'a' + 0xa),
86        b'A'..=b'F' => Some(c - b'A' + 0xa),
87        b'0'..=b'9' => Some(c - b'0'),
88        _ => None,
89    }
90}
91
92fn str_to_digest(s: String, digest: &mut [u8]) -> Result<(), XmlError> {
93    let l = digest.len();
94    if s.len() != l * 2 {
95        return Err(XmlError::InvalidChecksum(format!(
96            "No enough chars: {} {}",
97            s,
98            s.len()
99        )));
100    }
101
102    for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
103        let hi = match hexdigit_to_u8(chunk[0]) {
104            Some(v) => v,
105            None => return Err(XmlError::InvalidChecksum(s)),
106        };
107        let lo = match hexdigit_to_u8(chunk[1]) {
108            Some(v) => v,
109            None => return Err(XmlError::InvalidChecksum(s)),
110        };
111        digest[i] = hi << 4 | lo;
112    }
113
114    Ok(())
115}
116
117pub(crate) fn from_xml(xml: &str) -> Result<crate::bmap::Bmap, XmlError> {
118    let b: Bmap = from_str(xml)?;
119    let mut builder = BmapBuilder::default();
120    let hash_type = b.checksum_type;
121    builder
122        .image_size(b.image_size)
123        .block_size(b.block_size)
124        .blocks(b.blocks_count)
125        .checksum_type(hash_type)
126        .mapped_blocks(b.mapped_blocks_count);
127
128    for range in b.block_map.ranges {
129        let mut split = range.range.trim().splitn(2, '-');
130        let start = match split.next() {
131            Some(s) => s.parse().unwrap(),
132            None => unimplemented!("woops"),
133        };
134        let end = match split.next() {
135            Some(s) => s.parse().unwrap(),
136            None => start,
137        };
138
139        let checksum = match hash_type {
140            HashType::Sha256 => {
141                let mut v = [0; 32];
142                str_to_digest(range.chksum, &mut v)?;
143                HashValue::Sha256(v)
144            }
145        };
146        builder.add_block_range(start, end, checksum);
147    }
148
149    builder.build().map_err(std::convert::Into::into)
150}