mirror of
https://gitea.tendokyu.moe/beerpsi/icf-reader.git
synced 2024-11-23 22:50:59 +01:00
feat: allow version to be specified as a string
specifying a { major, minor, build } object still works.
This commit is contained in:
parent
d00b5d2a96
commit
5a3c7b784a
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
/target
|
||||
*.bin
|
||||
*.icf
|
||||
*.json
|
@ -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;
|
||||
|
@ -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,31 +118,27 @@ 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 {
|
||||
|
||||
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 {
|
||||
0x0000 | 0x0001 => {
|
||||
@ -153,11 +146,9 @@ 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 {
|
||||
if rd.read_bytes(16)?.iter().any(|b| *b != 0) {
|
||||
return Err(anyhow!("Padding error. Expected 16 NULL bytes."));
|
||||
}
|
||||
}
|
||||
|
||||
match container_type {
|
||||
0x0000 => IcfData::System(IcfInnerData {
|
||||
@ -182,11 +173,9 @@ 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 {
|
||||
if rd.read_bytes(16)?.iter().any(|b| *b != 0) {
|
||||
return Err(anyhow!("Padding error. Expected 16 NULL bytes."));
|
||||
}
|
||||
}
|
||||
|
||||
IcfData::Option(IcfOptionData {
|
||||
app_id: app_id.clone(),
|
||||
|
@ -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")]
|
||||
|
Loading…
Reference in New Issue
Block a user