alpm_mtree/mtree/v2.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
//! Interpreter for ALPM-MTREE v1 and v2.
use std::path::PathBuf;
use alpm_types::{Checksum, Digest, Md5Checksum, Sha256Checksum};
use serde::{Serialize, Serializer, ser::Error as SerdeError}; // codespell:ignore ser
use winnow::Parser;
pub use crate::parser::PathType;
use crate::{
Error,
parser::{self, SetProperty, UnsetProperty},
};
/// Represents a `/set` line in an MTREE file.
///
/// This struct also internally serves as the representation of default values
/// that're passed to all following path type lines.
#[derive(Debug, Clone, Default)]
pub struct PathDefaults {
uid: Option<usize>,
gid: Option<usize>,
mode: Option<String>,
path_type: Option<PathType>,
}
impl PathDefaults {
/// Apply a parsed `/set` statement's properties onto the current set of [PathDefaults].
fn apply_set(&mut self, properties: Vec<SetProperty>) {
for property in properties {
match property {
SetProperty::Uid(uid) => self.uid = Some(uid),
SetProperty::Gid(gid) => self.gid = Some(gid),
SetProperty::Mode(mode) => self.mode = Some(mode.to_string()),
SetProperty::Type(path_type) => self.path_type = Some(path_type),
}
}
}
/// Apply a parsed `/unset` statement's properties onto the current set of [PathDefaults].
fn apply_unset(&mut self, properties: Vec<UnsetProperty>) {
for property in properties {
match property {
UnsetProperty::Uid => self.uid = None,
UnsetProperty::Gid => self.gid = None,
UnsetProperty::Mode => self.mode = None,
UnsetProperty::Type => self.path_type = None,
}
}
}
}
/// A directory type path statement in an mtree file.
#[derive(Debug, Clone, Serialize)]
pub struct Directory {
path: PathBuf,
uid: usize,
gid: usize,
mode: String,
time: usize,
}
/// A file type path statement in an mtree file.
///
/// The md5_digest is accepted for backwards compatibility reasons in v2 as well.
#[derive(Debug, Clone, Serialize)]
pub struct File {
path: PathBuf,
uid: usize,
gid: usize,
mode: String,
size: usize,
time: usize,
#[serde(
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_optional_checksum_as_hex"
)]
md5_digest: Option<Md5Checksum>,
#[serde(serialize_with = "serialize_checksum_as_hex")]
sha256_digest: Sha256Checksum,
}
/// Serialize an `Option<Checksum<D>>` as a HexString.
///
/// # Errors
///
/// Returns an error if the `checksum` can not be serialized using the `serializer`.
fn serialize_checksum_as_hex<S, D>(checksum: &Checksum<D>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
D: Digest,
{
let hex_string = checksum.to_string();
serializer.serialize_str(&hex_string)
}
/// Serialize an `Option<Checksum<D>>`
///
/// Sadly this is needed in addition to the function above, even though we know that it won't be
/// called due to the `skip_serializing_if` check above.
fn serialize_optional_checksum_as_hex<S, D>(
checksum: &Option<Checksum<D>>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
D: Digest,
{
let hex_string = checksum
.as_ref()
.ok_or_else(|| S::Error::custom("Empty checksums won't be serialized"))?
.to_string();
serializer.serialize_str(&hex_string)
}
/// A link type path in an mtree file that points to a file somewhere on the system.
#[derive(Debug, Clone, Serialize)]
pub struct Link {
path: PathBuf,
uid: usize,
gid: usize,
mode: String,
time: usize,
link_path: PathBuf,
}
/// Represents the three possible types inside a path type line of an MTREE file.
///
/// While serializing, the type is converted into a `type` field on the inner struct.
/// This means that `Vec<Path>` will be serialized to a list of maps where each map has a `type`
/// entry with the respective name.
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type")]
pub enum Path {
#[serde(rename = "dir")]
Directory(Directory),
#[serde(rename = "file")]
File(File),
#[serde(rename = "link")]
Link(Link),
}
/// Parse the content of an MTREE v2 file.
///
/// This parser is backwards compatible to `v1`, in the sense that it allows `md5` checksums, but
/// doesn't require them.
///
/// # Example
///
/// ```
/// use alpm_mtree::mtree::v2::parse_mtree_v2;
///
/// # fn main() -> Result<(), alpm_mtree::Error> {
/// let content = r#"
/// /set uid=0 gid=0 mode=644 type=link
/// ./some_link link=/etc time=1706086640.0
/// "#;
/// let paths = parse_mtree_v2(content.to_string())?;
/// # Ok(())
/// # }
/// ```
///
/// # Errors
///
/// - `Error::ParseError` if a malformed MTREE file is encountered.
/// - `Error::InterpreterError` if there's missing fields or logical error in the parsed contents of
/// the MTREE file.
pub fn parse_mtree_v2(content: String) -> Result<Vec<Path>, Error> {
let parsed_contents = parser::mtree
.parse(&content)
.map_err(|err| Error::ParseError(format!("{err}")))?;
paths_from_parsed_content(&content, parsed_contents)
}
/// Take unsanitized parsed content and convert it to a list of paths with properties.
///
/// This is effectively the interpreter step for mtree's declaration language.
fn paths_from_parsed_content(
content: &str,
parsed_content: Vec<parser::Statement>,
) -> Result<Vec<Path>, Error> {
let mut paths = Vec::new();
// Track the current default properties for paths.
let mut path_defaults = PathDefaults::default();
for (line_nr, parsed) in parsed_content.into_iter().enumerate() {
match parsed {
parser::Statement::Ignored => continue,
parser::Statement::Path { path, properties } => {
// Create a [Path] instance from a given path statement.
// Pass the content and line-nr through.
// The line nr is incremented by one due to `#mtree` being the first line.
let path = path_from_parsed(content, line_nr, &path_defaults, path, properties)?;
paths.push(path);
}
parser::Statement::Set(properties) => {
// Incorporate a new `/set` command into the current set of defaults.
path_defaults.apply_set(properties);
}
parser::Statement::Unset(properties) => {
// Incorporate a new `/unset` command into the current set of defaults.
path_defaults.apply_unset(properties);
}
}
}
Ok(paths)
}
/// Return the nth line of a given file's content.
///
/// # Panics
///
/// Panics if `line_nr` refers to a line, that does not exist in `content`.
/// This is unlikely to ever happen, as the `content` is derived from a parsed file and therefore it
/// is known that the specific line referenced by `line_nr` exists.
fn content_line(content: &str, line_nr: usize) -> String {
let line = content.lines().nth(line_nr);
let Some(line) = line else {
unreachable!(
"Failed to read {line_nr} while handling an error. This should not happen, please report it as an issue."
);
};
line.to_string()
}
/// Take any given property and ensure that it's set.
///
/// # Errors
///
/// - `Error::InterpreterError` if the expected property is `None`.
fn ensure_property<T>(
content: &str,
line_nr: usize,
property: Option<T>,
property_name: &str,
) -> Result<T, Error> {
// Ensure that we know the type of the given path.
let Some(property) = property else {
return Err(Error::InterpreterError(
line_nr,
content_line(content, line_nr),
format!("Couldn't find property {property_name} for path."),
));
};
Ok(property)
}
/// Create the actual final MTREE path representation from the parsed input.
///
/// This is the core of the mtree interpreter logic and does a few critical things:
/// - Incorporate default properties specified by previous `/set` and `/unset` statements.
/// - Ensure all paths have all necessary properties for the given path type.
///
/// The way this works is as follows:
/// Go through all given properties and collect them in local `Option<T>` variables.
/// Afterwards, look at the `path_type` and ensure that all necessary properties for the given
/// path type are set.
/// If all properties are there, initialize the respective [Path] type and return it.
///
/// The original content (`content`), as well as line number (`line_nr`) are passed in as well to
/// provide detailed error messages.
///
/// # Errors
///
/// - `Error::InterpreterError` if expected properties for a given type aren't set.
fn path_from_parsed(
content: &str,
line_nr: usize,
defaults: &PathDefaults,
path: PathBuf,
properties: Vec<parser::PathProperty>,
) -> Result<Path, Error> {
// Copy any possible default values over.
let mut uid: Option<usize> = defaults.uid;
let mut gid: Option<usize> = defaults.gid;
let mut mode: Option<String> = defaults.mode.clone();
let mut path_type: Option<PathType> = defaults.path_type;
let mut link: Option<PathBuf> = None;
let mut size: Option<usize> = None;
let mut md5_digest: Option<Md5Checksum> = None;
let mut sha256_digest: Option<Sha256Checksum> = None;
let mut time: Option<usize> = None;
// Read all properties and set them accordingly.
for property in properties {
match property {
parser::PathProperty::Uid(inner) => uid = Some(inner),
parser::PathProperty::Gid(inner) => gid = Some(inner),
parser::PathProperty::Mode(inner) => mode = Some(inner.to_string()),
parser::PathProperty::Type(inner) => path_type = Some(inner),
parser::PathProperty::Size(inner) => size = Some(inner),
parser::PathProperty::Link(inner) => link = Some(inner),
parser::PathProperty::Md5Digest(checksum) => md5_digest = Some(checksum),
parser::PathProperty::Sha256Digest(checksum) => sha256_digest = Some(checksum),
parser::PathProperty::Time(inner) => time = Some(inner),
}
}
// Ensure that we know the type of the given path.
let Some(path_type) = path_type else {
return Err(Error::InterpreterError(
line_nr,
content_line(content, line_nr),
"Found no type for path.".to_string(),
));
};
// Build the path based on the path type.
// While doing so, ensure that all required properties are set.
let path = match path_type {
PathType::Dir => Path::Directory(Directory {
path,
uid: ensure_property(content, line_nr, uid, "uid")?,
gid: ensure_property(content, line_nr, gid, "gid")?,
mode: ensure_property(content, line_nr, mode, "mode")?,
time: ensure_property(content, line_nr, time, "time")?,
}),
PathType::File => Path::File(File {
path,
uid: ensure_property(content, line_nr, uid, "uid")?,
gid: ensure_property(content, line_nr, gid, "gid")?,
mode: ensure_property(content, line_nr, mode, "mode")?,
size: ensure_property(content, line_nr, size, "size")?,
time: ensure_property(content, line_nr, time, "time")?,
md5_digest,
sha256_digest: ensure_property(content, line_nr, sha256_digest, "sha256_digest")?,
}),
PathType::Link => Path::Link(Link {
path,
uid: ensure_property(content, line_nr, uid, "uid")?,
gid: ensure_property(content, line_nr, gid, "gid")?,
mode: ensure_property(content, line_nr, mode, "mode")?,
link_path: ensure_property(content, line_nr, link, "link")?,
time: ensure_property(content, line_nr, time, "time")?,
}),
};
Ok(path)
}