1use std::io::{self, Cursor, Read, Seek, SeekFrom};
2
3use byteorder::{BigEndian, ReadBytesExt};
4use hex;
5use log;
6
7const SFO_TABLE_ID: u32 = 0x1000;
9const ICON0_TABLE_ID: u32 = 0x1200;
10const FILE_DESCRIPTOR: &[u8; 4] = b"\x7fCNT";
11
12pub fn get_pkg_info(file_buf: &Vec<u8>) -> io::Result<PackageInfo> {
41 let mut file_cursor = Cursor::new(file_buf);
43 let mut base_buf = vec![0; 0x1f];
44
45 file_cursor.read(&mut base_buf)?;
46
47 let cnt_buf = base_buf[..0x4].to_vec();
49 if cnt_buf != FILE_DESCRIPTOR {
51 panic!("Invalid file format");
52 }
53
54 let table_total = u32::from_be_bytes(base_buf[0x10..0x14].try_into().unwrap());
55 let table_offset = u32::from_be_bytes(base_buf[0x18..0x1c].try_into().unwrap());
56 let table_size = table_total * 32;
57 let mut table_buf = vec![0; table_size as usize];
58
59 file_cursor.seek(SeekFrom::Start(table_offset as u64))?;
60 file_cursor.read_exact(&mut table_buf)?;
61
62 let mut param_sfo_table_entry = PkgTableEntry {
64 id: 0,
65 offset: 0,
66 size: 0,
67 };
68
69 log::debug!("{} files found", table_total);
78
79 for i in 0..table_total {
81 let slice = table_buf[(i * 32) as usize..((i * 32) + 32) as usize].to_vec();
82 let pkg_table_entry = get_pkg_table_entry(slice);
83 if pkg_table_entry.id == SFO_TABLE_ID {
84 log::debug!("params.sfo found!");
86 param_sfo_table_entry = pkg_table_entry.clone();
87 }
88 if pkg_table_entry.id == ICON0_TABLE_ID {
89 log::debug!("icon0.png found!");
91 }
94 }
95
96 file_cursor.seek(SeekFrom::Start(param_sfo_table_entry.offset as u64))?;
107 let mut param_sfo_buf = vec![0; param_sfo_table_entry.size as usize];
109 file_cursor.read_exact(&mut param_sfo_buf)?;
111 log::debug!("param.sfo size: {:?}", param_sfo_buf.len());
112
113 let param_sfo_header = get_param_sfo_header(¶m_sfo_buf);
114 let param_sfo_labels = param_sfo_buf
115 [param_sfo_header.label_ptr as usize..param_sfo_table_entry.size as usize]
116 .to_vec();
117 let param_sfo_data = param_sfo_buf
118 [param_sfo_header.data_ptr as usize..param_sfo_table_entry.size as usize]
119 .to_vec();
120
121 let mut pkg_info = PackageInfo {
122 app_type: PackageInfoAppType::Unknown,
123 app_ver: "".to_string(),
124 attribute: 0,
125 category: Category::AdditionalContent,
126 content_id: "".to_string(),
127 download_data_size: 0,
128 pubtoolinfo: "".to_string(),
129 pubtoolver: 0,
130 system_ver: 0,
131 title: "".to_string(),
132 title_id: "".to_string(),
133 version: "".to_string(),
134 };
136
137 let mut section_offset = 20;
138 let section_size = 16;
139
140 for _i in 0..param_sfo_header.section_total {
141 let param_sfo_section_buf =
142 param_sfo_buf[section_offset as usize..section_offset + section_size].to_vec();
143 let param_sfo_section = get_param_sfo_section(param_sfo_section_buf);
144
145 let mut label_buf = Vec::new();
146
147 for &byte in param_sfo_labels[param_sfo_section.label_offset as usize..].iter() {
149 if byte == 0 {
150 break; }
152 label_buf.push(byte); }
154
155 let label = String::from_utf8(label_buf).unwrap_or("".to_string());
156
157 let value_str = match param_sfo_section.data_type {
158 ParamSfoSectionDataType::String => {
159 let value_buf = param_sfo_data[param_sfo_section.data_offset as usize
160 ..(param_sfo_section.data_offset + param_sfo_section.used_data_field) as usize]
161 .to_vec();
162 String::from_utf8(value_buf).unwrap_or("".to_string())
163 }
164 ParamSfoSectionDataType::Integer => {
165 let value_buf = param_sfo_data[param_sfo_section.data_offset as usize
166 ..(param_sfo_section.data_offset + param_sfo_section.used_data_field) as usize]
167 .to_vec();
168 u32::from_le_bytes(value_buf[0..4].try_into().unwrap()).to_string()
169 }
170 _ => "".to_string(),
171 };
172 log::debug!("{}: {}", label, value_str);
173
174 match label.as_str() {
175 "APP_TYPE" => pkg_info.app_type = PackageInfoAppType::from(value_str.parse().unwrap_or(0)),
176 "APP_VER" => pkg_info.app_ver = value_str,
177 "ATTRIBUTE" => pkg_info.attribute = value_str.parse().unwrap_or(0),
178 "CONTENT_ID" => pkg_info.content_id = value_str,
180 "DOWNLOAD_DATA_SIZE" => pkg_info.download_data_size = value_str.parse().unwrap_or(0),
181 "PUBTOOLINFO" => pkg_info.pubtoolinfo = value_str,
182 "PUBTOOLVER" => pkg_info.pubtoolver = value_str.parse().unwrap_or(0),
183 "SYSTEM_VER" => pkg_info.system_ver = value_str.parse().unwrap_or(0),
184 "TITLE" => pkg_info.title = value_str,
185 "TITLE_ID" => pkg_info.title_id = value_str,
186 "VERSION" => pkg_info.version = value_str,
187 &_ => {
188 }
190 }
191 section_offset += section_size;
192 }
193
194 Ok(pkg_info)
195}
196
197fn get_pkg_table_entry(slice: Vec<u8>) -> PkgTableEntry {
198 let mut cursor = Cursor::new(slice);
199
200 let values: Vec<u32> = (0..6)
201 .map(|_| cursor.read_u32::<BigEndian>().unwrap())
202 .collect();
203
204 PkgTableEntry {
205 id: values[0],
206 offset: values[4],
207 size: values[5],
208 }
209}
210
211fn get_param_sfo_section(slice: Vec<u8>) -> ParamSfoSection {
212 let data_type_hex_str = hex::encode(&slice[3..4]); let data_type = u16::from_str_radix(&data_type_hex_str, 16).unwrap_or(0);
217
218 ParamSfoSection {
219 label_offset: u16::from_le_bytes(slice[0..2].try_into().unwrap()),
220 data_type: ParamSfoSectionDataType::from_u16(data_type),
221 used_data_field: u32::from_le_bytes(slice[4..8].try_into().unwrap()),
222 data_offset: u32::from_le_bytes(slice[12..16].try_into().unwrap()),
223 }
224}
225fn get_param_sfo_header(slice: &Vec<u8>) -> ParamSfoHeader {
226 ParamSfoHeader {
227 label_ptr: u32::from_le_bytes(slice[8..12].try_into().unwrap()),
228 data_ptr: u32::from_le_bytes(slice[12..16].try_into().unwrap()),
229 section_total: u32::from_le_bytes(slice[16..20].try_into().unwrap()),
230 }
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234struct ParamSfoHeader {
235 label_ptr: u32,
236 data_ptr: u32,
237 section_total: u32,
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241struct PkgTableEntry {
242 id: u32,
243 offset: u32,
244 size: u32,
245}
246
247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub enum PackageInfoAppType {
249 Unknown,
250 PaidStandaloneFull,
251 Upgradable,
252 Demo,
253 Freemium,
254}
255
256impl From<u8> for PackageInfoAppType {
257 fn from(value: u8) -> Self {
258 match value {
259 0 => Self::Unknown,
260 1 => Self::PaidStandaloneFull,
261 2 => Self::Upgradable,
262 3 => Self::Demo,
263 4 => Self::Freemium,
264 _ => Self::Unknown,
265 }
266 }
267}
268
269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
270pub enum Category {
271 AdditionalContent,
272 BluRayDisc,
273 GameContent,
274 GameDigital,
275 SystemApp,
276 BigApp,
277 BGApp,
278 MiniApp,
279 VideoServiceWebApp,
280 PSCloudBetaApp,
281 PS2,
282 GameApplicationPatch,
283 BigAppPatch,
284 BGAppPatch,
285 MiniAppPatch,
286 VideoServiceWebAppPatch,
287 PSCloudBetaAppPatch,
288 SaveData,
289}
290
291impl From<&str> for Category {
292 fn from(value: &str) -> Self {
293 match value {
294 "ac" => Self::AdditionalContent,
295 "bd" => Self::BluRayDisc,
296 "gc" => Self::GameContent,
297 "gd" => Self::GameDigital,
298 "gda" => Self::SystemApp,
299 "gdc" => Self::BigApp,
300 "gdd" => Self::BGApp,
301 "gde" => Self::MiniApp,
302 "gdk" => Self::VideoServiceWebApp,
303 "gdl" => Self::PSCloudBetaApp,
304 "gdO" => Self::PS2,
305 "gp" => Self::GameApplicationPatch,
306 "gpc" => Self::BigAppPatch,
307 "gpd" => Self::BGAppPatch,
308 "gpe" => Self::MiniAppPatch,
309 "gpk" => Self::VideoServiceWebAppPatch,
310 "gpl" => Self::PSCloudBetaAppPatch,
311 "sd" => Self::SaveData,
312 _ => panic!("Unknown category"),
313 }
314 }
315}
316
317#[derive(Debug, Clone, PartialEq, Eq)]
318pub struct PackageInfo {
319 pub app_type: PackageInfoAppType,
320 pub app_ver: String,
321 pub attribute: u32,
322 pub category: Category,
323 pub content_id: String,
324 pub download_data_size: u64,
325 pub pubtoolinfo: String,
326 pub pubtoolver: u32,
327 pub system_ver: u32,
328 pub title: String,
329 pub title_id: String,
330 pub version: String,
331 }
333
334impl PackageInfo {
335 pub fn from_buffer(buf: &Vec<u8>) -> Self {
336 get_pkg_info(buf).unwrap()
337 }
338}
339
340#[derive(Debug, Clone, Copy, PartialEq, Eq)]
341enum ParamSfoSectionDataType {
342 Binary,
343 String,
344 Integer,
345}
346
347impl ParamSfoSectionDataType {
348 fn from_u16(value: u16) -> Self {
350 match value {
351 0 => Self::Binary,
352 2 => Self::String,
353 4 => Self::Integer,
354 _ => panic!("Unknown category"),
355 }
357 }
358}
359
360#[derive(Debug, Clone, Copy, PartialEq, Eq)]
361struct ParamSfoSection {
362 label_offset: u16,
363 data_type: ParamSfoSectionDataType,
364 used_data_field: u32,
365 data_offset: u32,
366}