ps4_pkg_info/
lib.rs

1use std::io::{self, Cursor, Read, Seek, SeekFrom};
2
3use byteorder::{BigEndian, ReadBytesExt};
4use hex;
5use log;
6
7// magic numbers
8const SFO_TABLE_ID: u32 = 0x1000;
9const ICON0_TABLE_ID: u32 = 0x1200;
10const FILE_DESCRIPTOR: &[u8; 4] = b"\x7fCNT";
11
12/// Get package information from a PS4 PKG file
13/// 
14/// # Arguments
15///    * `file_buf` - A vector of bytes representing the PKG file
16/// 
17/// # Returns
18///    * `PackageInfo` - A struct containing the package information
19/// 
20/// # Example
21/// ```rust
22/// use std::{fs::File, io::Read};
23/// 
24/// use ps4_pkg_info::get_pkg_info;
25/// use env_logger;
26/// 
27/// const SIZE_1MB: usize = 0x100000;
28/// 
29/// fn main() -> std::io::Result<()> {
30///   env_logger::init();
31///   let mut file = File::open("tests/example.pkg")?;
32///   let mut file_buffer: Vec<u8> = vec![0; SIZE_1MB]; // Read in 1MB of data
33///   file.read(&mut file_buffer)?;
34/// 
35///  let pkg_info = get_pkg_info(&file_buffer)?;
36///  log::info!("{:?}", pkg_info);
37///  Ok(())
38/// }
39/// ```
40pub fn get_pkg_info(file_buf: &Vec<u8>) -> io::Result<PackageInfo> {
41    // let mut file = File::open("PS4_PKGI13337_v0.87.3.pkg")?;
42    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    // read first 4 bytes into a buffer
48    let cnt_buf = base_buf[..0x4].to_vec();
49    // check if we have the magic number
50    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    // init empty param_sfo as PkgTableEntry
63    let mut param_sfo_table_entry = PkgTableEntry {
64        id: 0,
65        offset: 0,
66        size: 0,
67    };
68
69    // TODO: IMPLEMENT ICON0.PNG
70    // init empty icon0 as PkgTableEntry
71    // let mut icon0_table_entry = PkgTableEntry {
72    //     id: 0,
73    //     offset: 0,
74    //     size: 0,
75    // };
76
77    log::debug!("{} files found", table_total);
78
79    // iterate through each table entry
80    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            // we have found the sfo file_cursor.
85            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            // we have found the icon0 file_cursor.
90            log::debug!("icon0.png found!");
91            // TODO: IMPLEMENT ICON0.PNG
92            // icon0_table_entry = pkg_table_entry.clone();
93        }
94    }
95
96    // TODO: IMPLEMENT ICON0.PNG
97    // // seek to the offset of the icon0.png file
98    // file_cursor.seek(SeekFrom::Start(icon0_table_entry.offset as u64))?;
99    // // create a buffer to hold the icon0.png file
100    // let mut icon0_buf = vec![0; icon0_table_entry.size as usize];
101    // // read the icon0.png file into a buffer
102    // file_cursor.read_exact(&mut icon0_buf)?;
103    // log::debug!("icon0.png size: {:?}", icon0_buf.len());
104
105    // seek to the offset of the param.sfo file
106    file_cursor.seek(SeekFrom::Start(param_sfo_table_entry.offset as u64))?;
107    // create a buffer to hold the param.sfo file
108    let mut param_sfo_buf = vec![0; param_sfo_table_entry.size as usize];
109    // read the param.sfo file into a buffer
110    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(&param_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        // extra: HashMap::new(),
135    };
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        // start reading each byte from label offset
148        for &byte in param_sfo_labels[param_sfo_section.label_offset as usize..].iter() {
149            if byte == 0 {
150                break; // Stop reading when we encounter the target (stop) character, 0
151            }
152            label_buf.push(byte); // Otherwise, collect the byte
153        }
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            // "CATEGORY" => pkg_info.category = Category::from(value_str.as_str()),
179            "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                // pkg_info.extra.insert(label, value_str);
189            }
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    // Get the hexadecimal string representation of the byte slice (3rd to 4th byte)
213    let data_type_hex_str = hex::encode(&slice[3..4]); // Slice the array to get the range (3, 4)
214
215    // Parse the hex string to a number (u8, u16, u32, etc.)
216    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    // pub extra: HashMap<String, String>,
332}
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    // Define a function to convert u16 to ParamSfoSectionDataType
349    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            // _ => None,  // Return None if the value doesn't match any variant
356        }
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}