diff --git a/cargo-espflash/src/main.rs b/cargo-espflash/src/main.rs index 47789a7c..9fc6c1a0 100644 --- a/cargo-espflash/src/main.rs +++ b/cargo-espflash/src/main.rs @@ -174,8 +174,11 @@ fn main() -> Result<()> { .or_else(|| metadata.partition_table.as_deref()) { let path = fs::canonicalize(path).into_diagnostic()?; - let data = fs::read_to_string(path).into_diagnostic()?; - let table = PartitionTable::try_from_str(data)?; + let data = fs::read_to_string(path) + .into_diagnostic() + .wrap_err("Failed to open partition table")?; + let table = + PartitionTable::try_from_str(data).wrap_err("Failed to parse partition table")?; Some(table) } else { None diff --git a/espflash/Cargo.toml b/espflash/Cargo.toml index ec2122c3..68732056 100644 --- a/espflash/Cargo.toml +++ b/espflash/Cargo.toml @@ -26,6 +26,7 @@ slip-codec = "0.2.4" thiserror = "1.0.20" xmas-elf = "0.8.0" serde = { version = "1.0", features = ["derive"] } +serde_plain = "1" toml = "0.5" directories-next = "2.0.0" color-eyre = "0.5" diff --git a/espflash/src/error.rs b/espflash/src/error.rs index b2da0bac..2bfa5562 100644 --- a/espflash/src/error.rs +++ b/espflash/src/error.rs @@ -1,7 +1,7 @@ use crate::flasher::Command; use crate::image_format::ImageFormatId; +use crate::partition_table::{SubType, Type}; use crate::Chip; -use csv::Position; use miette::{Diagnostic, SourceOffset, SourceSpan}; use slip_codec::Error as SlipError; use std::fmt::{Display, Formatter}; @@ -266,15 +266,34 @@ impl ResultExt for Result { } } +#[derive(Debug, Error, Diagnostic)] +pub enum PartitionTableError { + #[error(transparent)] + #[diagnostic(transparent)] + Csv(#[from] CSVError), + #[error(transparent)] + #[diagnostic(transparent)] + Overlapping(#[from] OverlappingPartitionsError), + #[error(transparent)] + #[diagnostic(transparent)] + Duplicate(#[from] DuplicatePartitionsError), + #[error(transparent)] + #[diagnostic(transparent)] + InvalidSubType(#[from] InvalidSubTypeError), + #[error(transparent)] + #[diagnostic(transparent)] + UnalignedPartitionError(#[from] UnalignedPartitionError), +} + #[derive(Debug, Error, Diagnostic)] #[error("Malformed partition table")] #[diagnostic( - code(espflash::mallformed_partition_table), - help("See the espressif documentation for information on the partition table format: + code(espflash::partition_table::mallformed), + help("{}See the espressif documentation for information on the partition table format: -https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/partition-tables.html#creating-custom-tables") +https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/partition-tables.html#creating-custom-tables", self.help) )] -pub struct PartitionTableError { +pub struct CSVError { #[source_code] source: String, #[label("{}", self.hint)] @@ -282,16 +301,17 @@ pub struct PartitionTableError { hint: String, #[source] error: csv::Error, + help: String, } -impl PartitionTableError { +impl CSVError { pub fn new(error: csv::Error, source: String) -> Self { - let err_pos = match error.kind() { - csv::ErrorKind::Deserialize { pos: Some(pos), .. } => pos.clone(), - csv::ErrorKind::UnequalLengths { pos: Some(pos), .. } => pos.clone(), - _ => Position::new(), + let err_line = match error.kind() { + csv::ErrorKind::Deserialize { pos: Some(pos), .. } => pos.line(), + csv::ErrorKind::UnequalLengths { pos: Some(pos), .. } => pos.line(), + _ => 0, }; - let hint = match error.kind() { + let mut hint = match error.kind() { csv::ErrorKind::Deserialize { err, .. } => err.to_string(), csv::ErrorKind::UnequalLengths { expected_len, len, .. @@ -301,27 +321,135 @@ impl PartitionTableError { ), _ => String::new(), }; + let mut help = String::new(); + + // string matching is fragile but afaik there is no better way in this case + // and if it does break the error is still not bad + if hint == "data did not match any variant of untagged enum SubType" { + hint = "Unknown sub-type".into(); + help = format!( + "the following sub-types are supported: + {} for data partitions + {} for app partitions\n\n", + Type::Data.subtype_hint(), + Type::App.subtype_hint() + ) + } - // since csv doesn't give us the position in the line the error occurs, we highlight the entire line - let line_length = source - .lines() - .nth(err_pos.line() as usize - 1) - .unwrap() - .len() - .into(); - let err_span = SourceSpan::new(pos_to_offset(err_pos), line_length); + let err_span = line_to_span(&source, err_line as usize); - PartitionTableError { + CSVError { source, err_span, hint, error, + help, + } + } +} + +/// since csv doesn't give us the position in the line the error occurs, we highlight the entire line +/// +/// line starts at 1 +fn line_to_span(source: &str, line: usize) -> SourceSpan { + let line_length = source.lines().nth(line - 1).unwrap().len().into(); + SourceSpan::new(SourceOffset::from_location(source, line, 2), line_length) +} + +#[derive(Debug, Error, Diagnostic)] +#[error("Overlapping partitions")] +#[diagnostic(code(espflash::partition_table::overlapping))] +pub struct OverlappingPartitionsError { + #[source_code] + source_code: String, + #[label("This partition")] + partition1_span: SourceSpan, + #[label("overlaps with this partition")] + partition2_span: SourceSpan, +} + +impl OverlappingPartitionsError { + pub fn new(source: &str, partition1_line: usize, partition2_line: usize) -> Self { + OverlappingPartitionsError { + source_code: source.into(), + partition1_span: line_to_span(source, partition1_line), + partition2_span: line_to_span(source, partition2_line), } } } -fn pos_to_offset(pos: Position) -> SourceOffset { - (pos.byte() as usize).into() +#[derive(Debug, Error, Diagnostic)] +#[error("Duplicate partitions")] +#[diagnostic(code(espflash::partition_table::duplicate))] +pub struct DuplicatePartitionsError { + #[source_code] + source_code: String, + #[label("This partition")] + partition1_span: SourceSpan, + #[label("has the same {} as this partition", self.ty)] + partition2_span: SourceSpan, + ty: &'static str, +} + +impl DuplicatePartitionsError { + pub fn new( + source: &str, + partition1_line: usize, + partition2_line: usize, + ty: &'static str, + ) -> Self { + DuplicatePartitionsError { + source_code: source.into(), + partition1_span: line_to_span(source, partition1_line), + partition2_span: line_to_span(source, partition2_line), + ty, + } + } +} + +#[derive(Debug, Error, Diagnostic)] +#[error("Invalid subtype for type")] +#[diagnostic( + code(espflash::partition_table::invalid_type), + help("'{}' supports the following subtypes: {}", self.ty, self.ty.subtype_hint()) +)] +pub struct InvalidSubTypeError { + #[source_code] + source_code: String, + #[label("'{}' is not a valid subtype for '{}'", self.sub_type, self.ty)] + span: SourceSpan, + ty: Type, + sub_type: SubType, +} + +impl InvalidSubTypeError { + pub fn new(source: &str, line: usize, ty: Type, sub_type: SubType) -> Self { + InvalidSubTypeError { + source_code: source.into(), + span: line_to_span(source, line), + ty, + sub_type, + } + } +} + +#[derive(Debug, Error, Diagnostic)] +#[error("Unaligned partition")] +#[diagnostic(code(espflash::partition_table::unaligned))] +pub struct UnalignedPartitionError { + #[source_code] + source_code: String, + #[label("App partition is not aligned to 64k (0x10000)")] + span: SourceSpan, +} + +impl UnalignedPartitionError { + pub fn new(source: &str, line: usize) -> Self { + UnalignedPartitionError { + source_code: source.into(), + span: line_to_span(source, line), + } + } } #[derive(Debug, Error)] diff --git a/espflash/src/partition_table.rs b/espflash/src/partition_table.rs index 40c388b7..8acb2c65 100644 --- a/espflash/src/partition_table.rs +++ b/espflash/src/partition_table.rs @@ -1,15 +1,20 @@ +use crate::error::{ + CSVError, DuplicatePartitionsError, InvalidSubTypeError, OverlappingPartitionsError, + PartitionTableError, UnalignedPartitionError, +}; use md5::{Context, Digest}; use regex::Regex; -use serde::{Deserialize, Deserializer}; - -use crate::error::PartitionTableError; +use serde::{Deserialize, Deserializer, Serialize}; +use std::cmp::{max, min}; +use std::fmt::Write as _; +use std::fmt::{Display, Formatter}; use std::io::Write; +use std::ops::Rem; const MAX_PARTITION_LENGTH: usize = 0xC00; const PARTITION_TABLE_SIZE: usize = 0x1000; -const MAX_PARTITION_TABLE_ENTRIES: usize = 95; -#[derive(Copy, Clone, Debug, Deserialize)] +#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq)] #[repr(u8)] #[allow(dead_code)] #[serde(rename_all = "lowercase")] @@ -18,7 +23,46 @@ pub enum Type { Data = 0x01, } -#[derive(Copy, Clone, Debug, Deserialize)] +impl Type { + pub fn subtype_hint(&self) -> String { + match self { + Type::App => "'factory', 'ota_0' through 'ota_15' and 'test'".into(), + Type::Data => { + let types = [ + DataType::Ota, + DataType::Phy, + DataType::Nvs, + DataType::CoreDump, + DataType::NvsKeys, + DataType::EFuse, + DataType::EspHttpd, + DataType::Fat, + DataType::Spiffs, + ]; + + let mut out = format!("'{}'", serde_plain::to_string(&types[0]).unwrap()); + for ty in &types[1..types.len() - 2] { + let ser = serde_plain::to_string(&ty).unwrap(); + write!(&mut out, ", '{}'", ser).unwrap(); + } + + let ser = serde_plain::to_string(&types[types.len() - 1]).unwrap(); + write!(&mut out, " and '{}'", ser).unwrap(); + + out + } + } + } +} + +impl Display for Type { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let ser = serde_plain::to_string(self).unwrap(); + write!(f, "{}", ser) + } +} + +#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq)] #[repr(u8)] #[allow(dead_code)] #[serde(rename_all = "lowercase")] @@ -59,7 +103,7 @@ pub enum AppType { Test = 0x20, } -#[derive(Copy, Clone, Debug, Deserialize)] +#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq)] #[repr(u8)] #[allow(dead_code)] #[serde(rename_all = "lowercase")] @@ -76,7 +120,7 @@ pub enum DataType { Spiffs = 0x82, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, PartialEq, Copy, Clone)] #[allow(dead_code)] #[serde(untagged)] pub enum SubType { @@ -84,6 +128,17 @@ pub enum SubType { Data(DataType), } +impl Display for SubType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let ser = match self { + SubType::App(sub) => serde_plain::to_string(sub), + SubType::Data(sub) => serde_plain::to_string(sub), + } + .unwrap(); + write!(f, "{}", ser) + } +} + impl SubType { fn as_u8(&self) -> u8 { match self { @@ -147,14 +202,20 @@ impl PartitionTable { .trim(csv::Trim::All) .from_reader(data.trim().as_bytes()); - let mut partitions = Vec::with_capacity(MAX_PARTITION_TABLE_ENTRIES); - for partition in reader.deserialize() { - let partition: Partition = - partition.map_err(|e| PartitionTableError::new(e, data.clone()))?; + let mut partitions = Vec::with_capacity(data.lines().count()); + for record in reader.records() { + let record = record.map_err(|e| CSVError::new(e, data.clone()))?; + let position = record.position(); + let mut partition: Partition = record + .deserialize(None) + .map_err(|e| CSVError::new(e, data.clone()))?; + partition.line = position.map(|pos| pos.line() as usize); partitions.push(partition); } - Ok(Self { partitions }) + let table = Self { partitions }; + table.validate(&data)?; + Ok(table) } pub fn to_bytes(&self) -> Vec { @@ -184,6 +245,57 @@ impl PartitionTable { Ok(()) } + + fn validate(&self, source: &str) -> Result<(), PartitionTableError> { + for partition in &self.partitions { + if let Some(line) = &partition.line { + let expected_type = match partition.sub_type { + SubType::App(_) => Type::App, + SubType::Data(_) => Type::Data, + }; + if expected_type != partition.ty { + return Err(InvalidSubTypeError::new( + source, + *line, + partition.ty, + partition.sub_type, + ) + .into()); + } + if partition.ty == Type::App && partition.offset.rem(0x10000) != 0 { + return Err(UnalignedPartitionError::new(source, *line).into()); + } + } + } + + for partition1 in &self.partitions { + for partition2 in &self.partitions { + if let (Some(line1), Some(line2)) = (&partition1.line, &partition2.line) { + if line1 != line2 { + if partition1.overlaps(partition2) { + return Err( + OverlappingPartitionsError::new(source, *line1, *line2).into() + ); + } + if partition1.name == partition2.name { + return Err(DuplicatePartitionsError::new( + source, *line1, *line2, "name", + ) + .into()); + } + if partition1.sub_type == partition2.sub_type { + return Err(DuplicatePartitionsError::new( + source, *line1, *line2, "sub-type", + ) + .into()); + } + } + } + } + } + + Ok(()) + } } const PARTITION_SIZE: usize = 32; @@ -199,6 +311,8 @@ struct Partition { #[serde(deserialize_with = "deserialize_partition_offset_or_size")] size: u32, flags: Option, + #[serde(skip)] + line: Option, } impl Partition { @@ -219,6 +333,7 @@ impl Partition { offset, size, flags, + line: None, } } @@ -242,6 +357,10 @@ impl Partition { Ok(()) } + + fn overlaps(&self, other: &Partition) -> bool { + max(self.offset, other.offset) < min(self.offset + self.size, other.offset + other.size) + } } fn deserialize_partition_name<'de, D>(deserializer: D) -> Result