From 5a3c7b784aaf917de156e48f5f96e295bc45f34d Mon Sep 17 00:00:00 2001 From: beerpsi Date: Fri, 22 Mar 2024 16:06:06 +0700 Subject: [PATCH] feat: allow version to be specified as a string specifying a { major, minor, build } object still works. --- .gitignore | 3 +- README.md | 3 ++ src/icf/mod.rs | 47 ++++++++++++------------------- src/icf/models.rs | 71 +++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 91 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index 238c8d1..7d8fbc9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target *.bin -*.icf \ No newline at end of file +*.icf +*.json \ No newline at end of file diff --git a/README.md b/README.md index d364975..2b4f30c 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,9 @@ correct checksum is not needed. ## Creating an ICF file from JSON The JSON file is an array of `IcfData`, which follows the format below: ```typescript +// Version can also be specified as a string with *exactly* three components: +// - "80.54.01": OK +// - "80.54.01.00": NG interface Version { major: number; minor: number; diff --git a/src/icf/mod.rs b/src/icf/mod.rs index d0fffbb..3f6d058 100644 --- a/src/icf/mod.rs +++ b/src/icf/mod.rs @@ -31,17 +31,14 @@ pub fn fixup_icf(data: &mut [u8]) -> Result<()> { let entry_count = rd.read_u64()?; let expected_size = 0x40 * (entry_count + 1); - let actual_entry_count = if actual_size as u64 != expected_size { + + if actual_size as u64 != expected_size { println!("[WARN] Expected size {expected_size} ({entry_count} entries) does not match actual size {actual_size}, automatically fixing"); let actual_entry_count = actual_size as u64 / 0x40 - 1; data[16..24].copy_from_slice(&actual_entry_count.to_le_bytes()); - - actual_entry_count - } else { - entry_count - }; + } let _ = String::from_utf8(rd.read_bytes(4)?.to_vec())?; let _ = String::from_utf8(rd.read_bytes(3)?.to_vec())?; @@ -49,8 +46,8 @@ pub fn fixup_icf(data: &mut [u8]) -> Result<()> { let reported_container_crc = rd.read_u32()?; let mut checksum = 0; - for i in 1..=(actual_entry_count as usize) { - let container = &data[0x40 * i..0x40 * (i + 1)]; + + for container in data.chunks_exact(0x40).skip(1) { if container[0] == 2 && container[1] == 1 { checksum ^= crc32fast::hash(container); } @@ -110,8 +107,8 @@ pub fn parse_icf(data: impl AsRef<[u8]>) -> Result> { let reported_crc = rd.read_u32()?; let mut checksum = 0; - for i in 1..=entry_count { - let container = &decrypted[0x40 * i..0x40 * (i + 1)]; + + for container in decrypted.chunks_exact(0x40).skip(1) { if container[0] == 2 && container[1] == 1 { checksum ^= crc32fast::hash(container); } @@ -121,30 +118,26 @@ pub fn parse_icf(data: impl AsRef<[u8]>) -> Result> { return Err(anyhow!("Reported container CRC32 ({reported_crc:02X}) does not match actual checksum ({checksum:02X})")); } - for _ in 0..7 { - if rd.read_u32()? != 0 { - return Err(anyhow!("Padding error. Expected 28 NULL bytes.")); - } + if rd.read_bytes(28)?.iter().any(|b| *b != 0) { + return Err(anyhow!("Padding error. Expected 24 NULL bytes.")); } let mut entries: Vec = Vec::with_capacity(entry_count); for _ in 0..entry_count { - let sig = rd.read_u16()?; + let sig = rd.read_u32()?; + if sig != 0x0102 && sig != 0x0201 { return Err(anyhow!( "Container does not start with signature (0x0102 or 0x0201), byte {:#06x}", rd.pos )); } - let _ = rd.read_bytes(2)?; let is_prerelease = sig == 0x0201; - let container_type = rd.read_u32()?; - for _ in 0..3 { - if rd.read_u64()? != 0 { - return Err(anyhow!("Padding error. Expected 24 NULL bytes.")); - } + + if rd.read_bytes(24)?.iter().any(|b| *b != 0) { + return Err(anyhow!("Padding error. Expected 24 NULL bytes.")); } let data: IcfData = match container_type { @@ -153,10 +146,8 @@ pub fn parse_icf(data: impl AsRef<[u8]>) -> Result> { let datetime = decode_icf_datetime(&mut rd)?; let required_system_version = decode_icf_version(&mut rd)?; - for _ in 0..2 { - if rd.read_u64()? != 0 { - return Err(anyhow!("Padding error. Expected 16 NULL bytes.")); - } + if rd.read_bytes(16)?.iter().any(|b| *b != 0) { + return Err(anyhow!("Padding error. Expected 16 NULL bytes.")); } match container_type { @@ -182,10 +173,8 @@ pub fn parse_icf(data: impl AsRef<[u8]>) -> Result> { let datetime = decode_icf_datetime(&mut rd)?; let required_system_version = decode_icf_version(&mut rd)?; - for _ in 0..2 { - if rd.read_u64()? != 0 { - return Err(anyhow!("Padding error. Expected 16 NULL bytes.")); - } + if rd.read_bytes(16)?.iter().any(|b| *b != 0) { + return Err(anyhow!("Padding error. Expected 16 NULL bytes.")); } IcfData::Option(IcfOptionData { diff --git a/src/icf/models.rs b/src/icf/models.rs index b86e47b..59025c5 100644 --- a/src/icf/models.rs +++ b/src/icf/models.rs @@ -1,9 +1,9 @@ use std::fmt::Display; use chrono::NaiveDateTime; -use serde::{Deserialize, Serialize}; +use serde::{de, Deserialize, Serialize, Serializer}; -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize)] pub struct Version { pub major: u16, pub minor: u8, @@ -16,10 +16,69 @@ impl Display for Version { } } +impl Serialize for Version { + fn serialize(&self, serializer: S) -> Result where S: Serializer { + serializer.serialize_str(&self.to_string()) + } +} + +// Preserve backwards compatibility by allowing either +// ```json +// "version": "80.54.01" +// ``` +// or +// ```json +// "version": { +// "major": 80, +// "minor": 54, +// "build": 01, +// } +// ``` +fn deserialize_version<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de> +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrVersion { + String(String), + Version(Version), + } + + let s: StringOrVersion = de::Deserialize::deserialize(deserializer)?; + + match s { + StringOrVersion::String(s) => { + let parts = s.split('.').collect::>(); + + if parts.len() > 3 { + return Err(de::Error::custom("A version must have exactly three components.")); + } + + let Ok(major) = parts[0].parse::() else { + return Err(de::Error::custom("Major version must be a 16-bit unsigned integer.")); + }; + let Ok(minor) = parts[1].parse::() else { + return Err(de::Error::custom("Minor version must be a 8-bit unsigned integer.")); + }; + let Ok(build) = parts[2].parse::() else { + return Err(de::Error::custom("Build version must be a 8-bit unsigned integer.")); + }; + + Ok(Version { major, minor, build }) + }, + StringOrVersion::Version(v) => Ok(v) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct IcfInnerData { pub id: String, + + #[serde(deserialize_with = "deserialize_version")] pub version: Version, + + #[serde(deserialize_with = "deserialize_version")] pub required_system_version: Version, pub datetime: NaiveDateTime, @@ -34,7 +93,7 @@ pub struct IcfOptionData { pub option_id: String, - #[serde(skip, default = "empty_version")] + #[serde(skip, default = "empty_version", deserialize_with = "deserialize_version")] pub required_system_version: Version, pub datetime: NaiveDateTime, @@ -50,12 +109,18 @@ pub struct IcfPatchData { pub sequence_number: u8, + #[serde(deserialize_with = "deserialize_version")] pub source_version: Version, pub source_datetime: NaiveDateTime, + + #[serde(deserialize_with = "deserialize_version")] pub source_required_system_version: Version, + #[serde(deserialize_with = "deserialize_version")] pub target_version: Version, pub target_datetime: NaiveDateTime, + + #[serde(deserialize_with = "deserialize_version")] pub target_required_system_version: Version, #[serde(default = "default_is_prerelease")]