feat: allow version to be specified as a string

specifying a { major, minor, build } object still works.
This commit is contained in:
beerpsi 2024-03-22 16:06:06 +07:00
parent d00b5d2a96
commit 5a3c7b784a
4 changed files with 91 additions and 33 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
/target
*.bin
*.icf
*.icf
*.json

View File

@ -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;

View File

@ -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<Vec<IcfData>> {
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<Vec<IcfData>> {
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<IcfData> = 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<Vec<IcfData>> {
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<Vec<IcfData>> {
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 {

View File

@ -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<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 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<Version, D::Error>
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::<Vec<&str>>();
if parts.len() > 3 {
return Err(de::Error::custom("A version must have exactly three components."));
}
let Ok(major) = parts[0].parse::<u16>() else {
return Err(de::Error::custom("Major version must be a 16-bit unsigned integer."));
};
let Ok(minor) = parts[1].parse::<u8>() else {
return Err(de::Error::custom("Minor version must be a 8-bit unsigned integer."));
};
let Ok(build) = parts[2].parse::<u8>() 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")]