Refactor + allow creating ICFs from JSON

This commit is contained in:
beerpsi
2024-01-02 17:40:16 +07:00
parent a0808a7081
commit 0dff839654
9 changed files with 723 additions and 238 deletions
+74
View File
@@ -0,0 +1,74 @@
use aes::cipher::{block_padding::NoPadding, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
use anyhow::{anyhow, Result};
type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
type Aes128CbcEnc = cbc::Encryptor<aes::Aes128>;
pub const ICF_KEY: [u8; 16] = hex_literal::decode(&[env!("ICF_KEY").as_bytes()]);
pub const ICF_IV: [u8; 16] = hex_literal::decode(&[env!("ICF_IV").as_bytes()]);
/// Decrypts an ICF using the provided key and IV.
pub fn decrypt_icf(
data: &mut [u8],
key: impl AsRef<[u8]>,
iv: impl AsRef<[u8]>,
) -> Result<Vec<u8>> {
let size = data.len();
let mut decrypted = Vec::with_capacity(size);
for i in (0..size).step_by(4096) {
let from_start = i;
let bufsz = std::cmp::min(4096, size - from_start);
let buf = &data[i..i + bufsz];
let mut decbuf = vec![0; bufsz];
let cipher = Aes128CbcDec::new_from_slices(key.as_ref(), iv.as_ref())?;
cipher
.decrypt_padded_b2b_mut::<NoPadding>(buf, &mut decbuf)
.map_err(|err| anyhow!(err))?;
let xor1 = u64::from_le_bytes(decbuf[0..8].try_into()?) ^ (from_start as u64);
let xor2 = u64::from_le_bytes(decbuf[8..16].try_into()?) ^ (from_start as u64);
decrypted.extend(xor1.to_le_bytes());
decrypted.extend(xor2.to_le_bytes());
decrypted.extend(&decbuf[16..]);
}
Ok(decrypted)
}
/// Encrypts an ICF using the provided key and IV.
pub fn encrypt_icf(data: &[u8], key: impl AsRef<[u8]>, iv: impl AsRef<[u8]>) -> Result<Vec<u8>> {
let size = data.len();
let mut encrypted = Vec::with_capacity(size);
let mut to_be_encrypted = Vec::with_capacity(std::cmp::min(4096, size));
for i in (0..size).step_by(4096) {
let from_start = i;
let bufsz = std::cmp::min(4096, size - from_start);
let buf = &data[i..i + bufsz];
let mut encbuf = vec![0; bufsz];
let xor1 = u64::from_le_bytes(buf[0..8].try_into()?) ^ (from_start as u64);
let xor2 = u64::from_le_bytes(buf[8..16].try_into()?) ^ (from_start as u64);
to_be_encrypted.extend(xor1.to_le_bytes());
to_be_encrypted.extend(xor2.to_le_bytes());
to_be_encrypted.extend(&buf[16..]);
let cipher = Aes128CbcEnc::new_from_slices(key.as_ref(), iv.as_ref())?;
cipher
.encrypt_padded_b2b_mut::<NoPadding>(&to_be_encrypted, &mut encbuf)
.map_err(|err| anyhow!(err))?;
encrypted.extend(encbuf);
to_be_encrypted.clear();
}
Ok(encrypted)
}
+146 -169
View File
@@ -1,166 +1,22 @@
use std::fmt::Display;
pub mod crypto;
pub mod models;
pub mod parser;
use aes::cipher::{block_padding::NoPadding, BlockDecryptMut, KeyIvInit, BlockEncryptMut};
use anyhow::{anyhow, Result};
use binary_reader::{BinaryReader, Endian};
use chrono::{NaiveDate, NaiveDateTime};
use chrono::{Datelike, Timelike, NaiveDateTime};
type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
type Aes128CbcEnc = cbc::Encryptor<aes::Aes128>;
use self::crypto::{decrypt_icf, ICF_IV, ICF_KEY};
use self::models::{IcfData, IcfInnerData, IcfOptionData, IcfPatchData, Version};
use self::parser::{decode_icf_datetime, decode_icf_version};
pub const ICF_KEY: [u8; 16] = hex_literal::decode(&[env!("ICF_KEY").as_bytes()]);
pub const ICF_IV: [u8; 16] = hex_literal::decode(&[env!("ICF_IV").as_bytes()]);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Version {
pub major: u16,
pub minor: u8,
pub build: u8,
}
impl Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{:0>2}.{:0>2}", self.major, self.minor, self.build)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IcfInnerData {
pub id: String,
pub version: Version,
pub required_system_version: Version,
pub datetime: NaiveDateTime,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IcfOptionData {
pub app_id: String,
pub option_id: String,
pub required_system_version: Version,
pub datetime: NaiveDateTime,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IcfPatchData {
pub id: String,
pub source_version: Version,
pub target_version: Version,
pub required_system_version: Version,
pub datetime: NaiveDateTime,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IcfData {
System(IcfInnerData),
App(IcfInnerData),
Patch(IcfPatchData),
Option(IcfOptionData),
}
pub fn decrypt_icf(data: &mut [u8], key: impl AsRef<[u8]>, iv: impl AsRef<[u8]>) -> Result<Vec<u8>> {
let size = data.len();
let mut decrypted = Vec::with_capacity(size);
for i in (0..size).step_by(4096) {
let from_start = i;
let bufsz = std::cmp::min(4096, size - from_start);
let buf = &data[i..i + bufsz];
let mut decbuf = vec![0; bufsz];
let cipher = Aes128CbcDec::new_from_slices(key.as_ref(), iv.as_ref())?;
cipher
.decrypt_padded_b2b_mut::<NoPadding>(buf, &mut decbuf)
.map_err(|err| anyhow!(err))?;
let xor1 = u64::from_le_bytes(decbuf[0..8].try_into()?) ^ (from_start as u64);
let xor2 = u64::from_le_bytes(decbuf[8..16].try_into()?) ^ (from_start as u64);
decrypted.extend(xor1.to_le_bytes());
decrypted.extend(xor2.to_le_bytes());
decrypted.extend(&decbuf[16..]);
}
Ok(decrypted)
}
pub fn encrypt_icf(data: &[u8], key: impl AsRef<[u8]>, iv: impl AsRef<[u8]>) -> Result<Vec<u8>> {
let size = data.len();
let mut encrypted = Vec::with_capacity(size);
for i in (0..size).step_by(4096) {
let from_start = i;
let bufsz = std::cmp::min(4096, size - from_start);
let buf = &data[i..i + bufsz];
let mut to_be_encrypted = Vec::with_capacity(bufsz);
let mut encbuf = vec![0; bufsz];
let xor1 = u64::from_le_bytes(buf[0..8].try_into()?) ^ (from_start as u64);
let xor2 = u64::from_le_bytes(buf[8..16].try_into()?) ^ (from_start as u64);
to_be_encrypted.extend(xor1.to_le_bytes());
to_be_encrypted.extend(xor2.to_le_bytes());
to_be_encrypted.extend(&buf[16..]);
let cipher = Aes128CbcEnc::new_from_slices(key.as_ref(), iv.as_ref())?;
cipher
.encrypt_padded_b2b_mut::<NoPadding>(&to_be_encrypted, &mut encbuf)
.map_err(|err| anyhow!(err))?;
encrypted.extend(encbuf);
}
Ok(encrypted)
}
pub fn decode_icf_datetime_version(
rd: &mut BinaryReader,
) -> Result<(NaiveDateTime, Version)> {
let datetime = NaiveDate::from_ymd_opt(
rd.read_i16()? as i32,
rd.read_u8()? as u32,
rd.read_u8()? as u32,
)
.ok_or(anyhow!("Invalid date"))?
.and_hms_milli_opt(
rd.read_u8()? as u32,
rd.read_u8()? as u32,
rd.read_u8()? as u32,
rd.read_u8()? as u32,
)
.ok_or(anyhow!("Invalid time"))?;
let required_system_version = Version {
build: rd.read_u8()?,
minor: rd.read_u8()?,
major: rd.read_u16()?,
};
Ok((datetime, required_system_version))
}
pub fn decode_icf_version(
rd: &mut BinaryReader,
) -> Result<Version> {
let version = Version {
build: rd.read_u8()?,
minor: rd.read_u8()?,
major: rd.read_u16()?,
};
Ok(version)
}
/// Fixes incorrect CRC32s caused by hex editing the ICF
/// Fixes incorrect metadata caused by hex editing the ICF
pub fn fixup_icf(data: &mut [u8]) -> Result<()> {
let mut rd = BinaryReader::from_u8(data);
rd.endian = Endian::Little;
let reported_icf_crc = rd.read_u32()?;
let reported_size = rd.read_u32()?;
let actual_size = data.len() as u32;
if actual_size != reported_size {
@@ -179,7 +35,7 @@ pub fn fixup_icf(data: &mut [u8]) -> Result<()> {
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
@@ -275,7 +131,10 @@ pub fn parse_icf(data: impl AsRef<[u8]>) -> Result<Vec<IcfData>> {
for _ in 0..entry_count {
let sig = rd.read_bytes(4)?;
if sig[0] != 2 || sig[1] != 1 {
return Err(anyhow!("Container does not start with signature (0x0102), byte {:#06x}", rd.pos));
return Err(anyhow!(
"Container does not start with signature (0x0102), byte {:#06x}",
rd.pos
));
}
let container_type = rd.read_u32()?;
@@ -288,7 +147,8 @@ pub fn parse_icf(data: impl AsRef<[u8]>) -> Result<Vec<IcfData>> {
let data: IcfData = match container_type {
0x0000 | 0x0001 => {
let version = decode_icf_version(&mut rd)?;
let (datetime, required_system_version) = decode_icf_datetime_version(&mut rd)?;
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 {
@@ -309,12 +169,13 @@ pub fn parse_icf(data: impl AsRef<[u8]>) -> Result<Vec<IcfData>> {
datetime,
required_system_version,
}),
_ => unreachable!()
_ => unreachable!(),
}
}
0x0002 => {
let option_id = String::from_utf8(rd.read_bytes(4)?.to_vec())?;
let (datetime, required_system_version) = decode_icf_datetime_version(&mut rd)?;
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 {
@@ -328,28 +189,38 @@ pub fn parse_icf(data: impl AsRef<[u8]>) -> Result<Vec<IcfData>> {
datetime,
required_system_version,
})
}
0x0101 => {
_ => {
// PATCH container type also encode the patch's sequence number
// in the higher 16 bits.
// The lower 16 bits will always be 1.
let sequence_number = (container_type >> 8) as u8;
if (container_type & 1) == 0 || sequence_number == 0 {
println!("Unknown ICF container type {container_type:#06x} at byte {:#06x}, skipping", rd.pos);
rd.read_bytes(32)?;
continue;
}
let target_version = decode_icf_version(&mut rd)?;
let (datetime, required_system_version) = decode_icf_datetime_version(&mut rd)?;
let target_datetime = decode_icf_datetime(&mut rd)?;
let target_required_system_version = decode_icf_version(&mut rd)?;
let source_version = decode_icf_version(&mut rd)?;
let (_, _) = decode_icf_datetime_version(&mut rd)?;
let source_datetime = decode_icf_datetime(&mut rd)?;
let source_required_system_version = decode_icf_version(&mut rd)?;
IcfData::Patch(IcfPatchData {
id: app_id.clone(),
sequence_number,
source_version,
source_datetime,
source_required_system_version,
target_version,
required_system_version,
datetime,
target_datetime,
target_required_system_version,
})
}
_ => {
println!("Unknown ICF container type {container_type:#06x} at byte {:#06x}, skipping", rd.pos);
rd.read_bytes(32)?;
continue;
}
};
entries.push(data);
@@ -363,3 +234,109 @@ pub fn decode_icf(data: &mut [u8]) -> Result<Vec<IcfData>> {
parse_icf(decrypted)
}
pub fn serialize_datetime(data: &mut Vec<u8>, datetime: NaiveDateTime) {
data.extend((datetime.year() as u16).to_le_bytes());
data.extend([
datetime.month() as u8,
datetime.day() as u8,
datetime.hour() as u8,
datetime.minute() as u8,
datetime.second() as u8,
0x00,
]);
}
pub fn serialize_version(data: &mut Vec<u8>, version: Version) {
data.extend([version.build, version.minor]);
data.extend(version.major.to_le_bytes());
}
pub fn serialize_icf(data: &[IcfData]) -> Result<Vec<u8>> {
let entry_count = data.len();
let icf_length = 0x40 * (entry_count + 1);
let mut icf: Vec<u8> = Vec::with_capacity(icf_length);
icf.extend([0x00; 0x40]);
let mut platform_id: Option<String> = None;
let mut app_id: Option<String> = None;
for container in data {
icf.extend([0x02, 0x01, 0x00, 0x00]);
match container {
IcfData::System(s) => {
platform_id = Some(s.id.clone());
icf.extend([0x00; 4]);
}
IcfData::App(a) => {
app_id = Some(a.id.clone());
icf.extend([0x01, 0x00, 0x00, 0x00]);
}
IcfData::Option(_) => {
icf.extend([0x02, 0x00, 0x00, 0x00]);
}
IcfData::Patch(p) => {
icf.extend([0x01, p.sequence_number, 0x00, 0x00]);
}
}
icf.extend([0x00; 24]);
if let IcfData::Option(o) = container {
icf.extend(o.option_id.as_bytes());
serialize_datetime(&mut icf, o.datetime);
icf.extend([0x00; 20]);
continue;
}
let (version, datetime, required_system_version) = match container {
IcfData::System(s) => (s.version, s.datetime, s.required_system_version),
IcfData::App(s) => (s.version, s.datetime, s.required_system_version),
IcfData::Patch(s) => (s.target_version, s.target_datetime, s.target_required_system_version),
IcfData::Option(_) => unreachable!(),
};
serialize_version(&mut icf, version);
serialize_datetime(&mut icf, datetime);
serialize_version(&mut icf, required_system_version);
if let IcfData::Patch(p) = container {
serialize_version(&mut icf, p.source_version);
serialize_datetime(&mut icf, p.source_datetime);
serialize_version(&mut icf, p.source_required_system_version);
} else {
icf.extend([0x00; 16]);
}
}
let platform_id = match platform_id {
Some(s) => s,
None => return Err(anyhow!("Missing entry of type System in provided ICF data")),
};
let app_id = match app_id {
Some(s) => s,
None => return Err(anyhow!("Missing entry of type App in provided ICF data")),
};
let mut containers_checksum: u32 = 0;
for container in icf.chunks(0x40).skip(1) {
if container[0] == 2 && container[1] == 1 {
containers_checksum ^= crc32fast::hash(container);
}
}
icf[4..8].copy_from_slice(&(icf_length as u32).to_le_bytes());
icf[16..24].copy_from_slice(&(entry_count as u64).to_le_bytes());
icf[24..28].copy_from_slice(app_id.as_bytes());
icf[28..31].copy_from_slice(platform_id.as_bytes());
icf[32..36].copy_from_slice(&containers_checksum.to_le_bytes());
let icf_crc = crc32fast::hash(&icf[4..]);
icf[0..4].copy_from_slice(&icf_crc.to_le_bytes());
Ok(icf)
}
+92
View File
@@ -0,0 +1,92 @@
use std::fmt::Display;
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Version {
pub major: u16,
pub minor: u8,
pub build: u8,
}
impl Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{:0>2}.{:0>2}", self.major, self.minor, self.build)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IcfInnerData {
pub id: String,
pub version: Version,
pub required_system_version: Version,
pub datetime: NaiveDateTime,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IcfOptionData {
pub app_id: String,
pub option_id: String,
pub required_system_version: Version,
pub datetime: NaiveDateTime,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IcfPatchData {
pub id: String,
pub sequence_number: u8,
pub source_version: Version,
pub source_datetime: NaiveDateTime,
pub source_required_system_version: Version,
pub target_version: Version,
pub target_datetime: NaiveDateTime,
pub target_required_system_version: Version,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum IcfData {
System(IcfInnerData),
App(IcfInnerData),
Patch(IcfPatchData),
Option(IcfOptionData),
}
impl IcfData {
pub fn filename(&self) -> String {
match self {
IcfData::System(data) => format!(
"{}_{:04}.{:02}.{:02}_{}_0.pack",
data.id,
data.version.major,
data.version.minor,
data.version.build,
data.datetime.format("%Y%m%d%H%M%S")
),
IcfData::App(data) => format!(
"{}_{}_{}_0.app",
data.id,
data.version,
data.datetime.format("%Y%m%d%H%M%S")
),
IcfData::Option(data) => format!(
"{}_{}_{}_0.opt",
data.app_id,
data.option_id,
data.datetime.format("%Y%m%d%H%M%S")
),
IcfData::Patch(data) => format!(
"{}_{}_{}_{}_{}.app",
data.id,
data.target_version,
data.target_datetime.format("%Y%m%d%H%M%S"),
data.sequence_number,
data.source_version,
),
}
}
}
+33
View File
@@ -0,0 +1,33 @@
use anyhow::{anyhow, Result};
use binary_reader::BinaryReader;
use chrono::{NaiveDate, NaiveDateTime};
use super::models::Version;
pub fn decode_icf_datetime(rd: &mut BinaryReader) -> Result<NaiveDateTime> {
let datetime = NaiveDate::from_ymd_opt(
rd.read_i16()? as i32,
rd.read_u8()? as u32,
rd.read_u8()? as u32,
)
.ok_or(anyhow!("Invalid date"))?
.and_hms_milli_opt(
rd.read_u8()? as u32,
rd.read_u8()? as u32,
rd.read_u8()? as u32,
rd.read_u8()? as u32,
)
.ok_or(anyhow!("Invalid time"))?;
Ok(datetime)
}
pub fn decode_icf_version(rd: &mut BinaryReader) -> Result<Version> {
let version = Version {
build: rd.read_u8()?,
minor: rd.read_u8()?,
major: rd.read_u16()?,
};
Ok(version)
}
+73 -50
View File
@@ -1,67 +1,90 @@
use std::{env, process::exit};
use anyhow::anyhow;
use icf::{decode_icf, decrypt_icf, ICF_KEY, ICF_IV, parse_icf, encrypt_icf, fixup_icf};
use crate::icf::IcfData;
mod icf;
use std::fs::File;
use clap::{Parser, Subcommand};
use icf::serialize_icf;
use crate::icf::{
crypto::{decrypt_icf, encrypt_icf, ICF_IV, ICF_KEY},
decode_icf, fixup_icf,
models::IcfData,
parse_icf,
};
#[derive(Parser, Debug)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
#[command(about = "Fixes some common ICF errors, then encrypt the given ICF")]
Encrypt { input: String, output: String },
#[command(about = "Decrypts the given ICF")]
Decrypt { input: String, output: String },
#[command(about = "Decodes the given ICF (optionally to a JSON file)")]
Decode {
icf: String,
json_output: Option<String>,
},
#[command(about = "Encodes a JSON file from the decode subcommand to an ICF")]
Encode {
json_input: String,
output: String,
},
}
fn main() -> Result<(), anyhow::Error> {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
println!("Usage: icf-reader.exe <PATH TO ICF>");
exit(1);
}
let cli = Cli::parse();
if args[1] == "decrypt" {
if args.len() < 4 {
println!("Usage: icf-reader.exe decrypt <PATH TO ICF> <OUTPUT>");
exit(1);
match &cli.command {
Commands::Encrypt { input, output } => {
let mut icf_buf = std::fs::read(input)?;
fixup_icf(&mut icf_buf)?;
let icf = parse_icf(&icf_buf)?;
for entry in icf {
println!("{}", entry.filename());
}
let encrypted_icf = encrypt_icf(&icf_buf, ICF_KEY, ICF_IV)?;
std::fs::write(output, encrypted_icf)?;
}
Commands::Decrypt { input, output } => {
let mut icf_buf = std::fs::read(input)?;
let decrypted_icf = decrypt_icf(&mut icf_buf, ICF_KEY, ICF_IV)?;
let mut icf_buf = std::fs::read(args[2].clone())?;
let decrypted_icf = decrypt_icf(&mut icf_buf, ICF_KEY, ICF_IV)?;
std::fs::write(args[3].clone(), decrypted_icf)?;
exit(0)
}
if args[1] == "encrypt" {
if args.len() < 4 {
println!("Usage: icf-reader.exe encrypt <PATH TO ICF> <OUTPUT>");
exit(1);
std::fs::write(output, decrypted_icf)?;
}
Commands::Decode { icf, json_output } => {
let mut icf_buf = std::fs::read(icf)?;
let icf = decode_icf(&mut icf_buf)?;
let mut icf_buf = std::fs::read(args[2].clone())?;
fixup_icf(&mut icf_buf)?;
let icf = parse_icf(&icf_buf)?;
if let Some(json_output) = json_output {
let f = File::create(json_output)?;
serde_json::to_writer_pretty(f, &icf)?;
}
for entry in icf {
match entry {
IcfData::System(data) => println!("{}_{:04}.{:02}.{:02}_{}_0.pack", data.id, data.version.major, data.version.minor, data.version.build, data.datetime.format("%Y%m%d%H%M%S")),
IcfData::App(data) => println!("{}_{}.{:02}.{:02}_{}_0.app", data.id, data.version.major, data.version.minor, data.version.build, data.datetime.format("%Y%m%d%H%M%S")),
IcfData::Option(data) => println!("{}_{}_{}_0.opt", data.app_id, data.option_id, data.datetime.format("%Y%m%d%H%M%S")),
IcfData::Patch(data) => println!("{}_{}.{:02}.{:02}_{}_1_{}.{:02}.{:02}.app", data.id, data.target_version.major, data.target_version.minor, data.target_version.build, data.datetime.format("%Y%m%d%H%M%S"), data.required_system_version.major, data.required_system_version.minor, data.required_system_version.build),
for entry in icf {
println!("{}", entry.filename())
}
}
Commands::Encode { json_input, output } => {
let input = std::fs::read_to_string(json_input)?;
let icf: Vec<IcfData> = serde_json::from_str(&input)?;
let out = serialize_icf(&icf)?;
let encrypted_icf = encrypt_icf(&icf_buf, ICF_KEY, ICF_IV)?;
std::fs::write("test.bin", &out)?;
std::fs::write(args[3].clone(), encrypted_icf)?;
let encrypted = encrypt_icf(&out, ICF_KEY, ICF_IV)?;
exit(0)
}
let mut icf_buf = std::fs::read(args[1].clone())?;
let icf = decode_icf(&mut icf_buf).map_err(|err| anyhow!("Reading ICF failed: {:#}", err))?;
for entry in icf {
match entry {
IcfData::System(data) => println!("{}_{:04}.{:02}.{:02}_{}_0.pack", data.id, data.version.major, data.version.minor, data.version.build, data.datetime.format("%Y%m%d%H%M%S")),
IcfData::App(data) => println!("{}_{}.{:02}.{:02}_{}_0.app", data.id, data.version.major, data.version.minor, data.version.build, data.datetime.format("%Y%m%d%H%M%S")),
IcfData::Option(data) => println!("{}_{}_{}_0.opt", data.app_id, data.option_id, data.datetime.format("%Y%m%d%H%M%S")),
IcfData::Patch(data) => println!("{}_{}.{:02}.{:02}_{}_1_{}.{:02}.{:02}.app", data.id, data.target_version.major, data.target_version.minor, data.target_version.build, data.datetime.format("%Y%m%d%H%M%S"), data.required_system_version.major, data.required_system_version.minor, data.required_system_version.build),
std::fs::write(output, encrypted)?;
}
}