mirror of
https://github.com/Ap0dexMe0/unixtract.git
synced 2026-07-15 21:00:03 +02:00
Add RUF extractor + improve epk3
This commit is contained in:
@@ -4,6 +4,7 @@ pub mod tpv_timg;
|
||||
pub mod pfl_upg;
|
||||
pub mod sddl_sec;
|
||||
pub mod novatek;
|
||||
pub mod ruf;
|
||||
|
||||
pub mod msd;
|
||||
pub mod msd10;
|
||||
|
||||
+57
-55
@@ -1,11 +1,55 @@
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::path::{Path};
|
||||
use std::io::{Write, Seek, SeekFrom, Cursor};
|
||||
use binrw::{BinRead, BinReaderExt};
|
||||
|
||||
use crate::common;
|
||||
use crate::keys;
|
||||
use crate::formats::epk::{decrypt_aes_ecb_auto, find_key};
|
||||
|
||||
#[derive(BinRead)]
|
||||
struct Header {
|
||||
#[br(count = 4)] _magic_bytes: Vec<u8>,
|
||||
#[br(count = 4)] version: Vec<u8>,
|
||||
#[br(count = 32)] ota_id_bytes: Vec<u8>,
|
||||
package_info_size: u32,
|
||||
}
|
||||
impl Header {
|
||||
fn ota_id(&self) -> String {
|
||||
common::string_from_bytes(&self.ota_id_bytes)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(BinRead)]
|
||||
struct PkgInfoHeader {
|
||||
package_info_list_size: u32,
|
||||
package_info_count: u32,
|
||||
}
|
||||
|
||||
#[derive(BinRead)]
|
||||
struct PkgInfoEntry {
|
||||
_package_type: u32,
|
||||
_package_info_size: u32,
|
||||
#[br(count = 128)] package_name_bytes: Vec<u8>,
|
||||
#[br(count = 96)] _package_version_bytes: Vec<u8>,
|
||||
#[br(count = 32)] _package_architecture_bytes: Vec<u8>,
|
||||
#[br(count = 32)] _checksum: Vec<u8>,
|
||||
package_size: u32,
|
||||
_dipk: u32,
|
||||
//segment info
|
||||
_is_segmented: u32,
|
||||
segment_index: u32,
|
||||
segment_count: u32,
|
||||
segment_size: u32,
|
||||
//
|
||||
_unk: u32,
|
||||
}
|
||||
impl PkgInfoEntry {
|
||||
fn package_name(&self) -> String {
|
||||
common::string_from_bytes(&self.package_name_bytes)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_epk3(mut file: &File, output_folder: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
file.seek(SeekFrom::Start(128))?; //inital signature
|
||||
|
||||
@@ -42,86 +86,45 @@ pub fn extract_epk3(mut file: &File, output_folder: &str) -> Result<(), Box<dyn
|
||||
|
||||
//parse header
|
||||
let mut hdr_reader = Cursor::new(header);
|
||||
|
||||
if new_type {let _signature = common::read_exact(&mut hdr_reader, 128)?;};
|
||||
|
||||
let _epk3 = common::read_exact(&mut hdr_reader, 4)?; //EPK3 magic
|
||||
|
||||
let version = common::read_exact(&mut hdr_reader, 4)?;
|
||||
|
||||
let ota_id_bytes = common::read_exact(&mut hdr_reader, 32)?;
|
||||
let ota_id = common::string_from_bytes(&ota_id_bytes);
|
||||
|
||||
let package_info_size_bytes = common::read_exact(&mut hdr_reader, 4)?;
|
||||
let package_info_size = u32::from_le_bytes(package_info_size_bytes.try_into().unwrap());
|
||||
let hdr: Header = hdr_reader.read_le()?;
|
||||
|
||||
println!("\nEPK info:\nOTA ID: {}\nVersion: {:02x?}.{:02x?}.{:02x?}\nPackage Info size: {}\n",
|
||||
ota_id, version[3], version[2], version[1], package_info_size);
|
||||
hdr.ota_id(), hdr.version[3], hdr.version[2], hdr.version[1], hdr.package_info_size);
|
||||
//
|
||||
|
||||
let _versions = common::read_exact(&mut file, 36)?;
|
||||
|
||||
let _signature = common::read_exact(&mut file, signature_size)?;
|
||||
|
||||
//PKG INFO
|
||||
let pkg_info_encrypted = common::read_exact(&mut file, package_info_size as usize)?;
|
||||
let pkg_info_encrypted = common::read_exact(&mut file, hdr.package_info_size as usize)?;
|
||||
let pkg_info = decrypt_aes_ecb_auto(matching_key_bytes, &pkg_info_encrypted)?;
|
||||
|
||||
let mut pkg_info_reader = Cursor::new(pkg_info);
|
||||
|
||||
let package_info_list_size_b = common::read_exact(&mut pkg_info_reader, 4)?;
|
||||
let package_info_list_size = u32::from_le_bytes(package_info_list_size_b.try_into().unwrap());
|
||||
|
||||
let package_info_count_b = common::read_exact(&mut pkg_info_reader, 4)?;
|
||||
let package_info_count = u32::from_le_bytes(package_info_count_b.try_into().unwrap());
|
||||
let pkg_info_hdr: PkgInfoHeader = pkg_info_reader.read_le()?;
|
||||
|
||||
println!("Package info list size: {}\nPackage info count: {}",
|
||||
package_info_list_size, package_info_count);
|
||||
pkg_info_hdr.package_info_list_size, pkg_info_hdr.package_info_count);
|
||||
|
||||
if new_type {let _unknown = common::read_exact(&mut pkg_info_reader, 4)?;}; //uncertain if this is only in new type, but i think it is
|
||||
if new_type {let _unknown = common::read_exact(&mut pkg_info_reader, 4)?;}; //new type has additional value
|
||||
|
||||
while (pkg_info_reader.position() as usize) < pkg_info_reader.get_ref().len() {
|
||||
let segment = common::read_exact(&mut pkg_info_reader, 324)?;
|
||||
|
||||
let package_name_b = &segment[8..136];
|
||||
let package_name = common::string_from_bytes(&package_name_b);
|
||||
|
||||
let package_size_b = &segment[296..300];
|
||||
let package_size = u32::from_le_bytes(package_size_b.try_into().unwrap());
|
||||
|
||||
//Package segment info
|
||||
let segment_index_b = &segment[308..312];
|
||||
let mut segment_index = u32::from_le_bytes(segment_index_b.try_into().unwrap());
|
||||
|
||||
let segment_count_b = &segment[312..316];
|
||||
let segment_count = u32::from_le_bytes(segment_count_b.try_into().unwrap());
|
||||
|
||||
let segment_size_b = &segment[316..320];
|
||||
let mut segment_size = u32::from_le_bytes(segment_size_b.try_into().unwrap());
|
||||
//
|
||||
let mut entry: PkgInfoEntry = pkg_info_reader.read_le()?;
|
||||
|
||||
println!("\nPak - {}, Size: {}, Segments: {}",
|
||||
package_name, package_size, segment_count);
|
||||
entry.package_name(), entry.package_size, entry.segment_count);
|
||||
|
||||
for i in 0..segment_count {
|
||||
for i in 0..entry.segment_count {
|
||||
if i > 0 {
|
||||
let segment = common::read_exact(&mut pkg_info_reader, 324)?;
|
||||
|
||||
let segment_index_b = &segment[308..312];
|
||||
segment_index = u32::from_le_bytes(segment_index_b.try_into().unwrap());
|
||||
|
||||
let segment_size_b = &segment[316..320];
|
||||
segment_size = u32::from_le_bytes(segment_size_b.try_into().unwrap());
|
||||
entry = pkg_info_reader.read_le()?;
|
||||
}
|
||||
|
||||
println!("- Segment {}/{}, Size: {}", segment_index + 1, segment_count, segment_size);
|
||||
println!("- Segment {}/{}, Size: {}", entry.segment_index + 1, entry.segment_count, entry.segment_size);
|
||||
|
||||
let _signature = common::read_exact(&mut file, signature_size)?;
|
||||
|
||||
let encrypted_data = common::read_exact(&mut file, segment_size as usize + extra_segment_size)?;
|
||||
let encrypted_data = common::read_exact(&mut file, entry.segment_size as usize + extra_segment_size)?;
|
||||
let out_data = decrypt_aes_ecb_auto(matching_key_bytes, &encrypted_data)?;
|
||||
|
||||
let output_path = Path::new(&output_folder).join(package_name.clone() + ".bin");
|
||||
let output_path = Path::new(&output_folder).join(entry.package_name().clone() + ".bin");
|
||||
|
||||
fs::create_dir_all(&output_folder)?;
|
||||
let mut out_file = OpenOptions::new()
|
||||
@@ -136,6 +139,5 @@ pub fn extract_epk3(mut file: &File, output_folder: &str) -> Result<(), Box<dyn
|
||||
}
|
||||
|
||||
println!("\nExtraction finished!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
use std::path::{Path};
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use binrw::{BinRead, BinReaderExt};
|
||||
use std::io::{Write, Seek, SeekFrom, Cursor};
|
||||
use aes::Aes128;
|
||||
use cbc::{Decryptor, cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit}};
|
||||
type Aes128CbcDec = Decryptor<Aes128>;
|
||||
|
||||
use crate::common;
|
||||
use crate::keys;
|
||||
|
||||
#[derive(BinRead)]
|
||||
struct RufHeader {
|
||||
#[br(count = 6)] _magic_bytes: Vec<u8>,
|
||||
#[br(count = 2)] _upgrade_type_bytes: Vec<u8>,
|
||||
_unk1: u32,
|
||||
#[br(count = 32)] date_time_bytes: Vec<u8>,
|
||||
#[br(count = 8)] buyer_bytes: Vec<u8>,
|
||||
#[br(count = 32)] model_bytes: Vec<u8>,
|
||||
#[br(count = 32)] region_info_bytes: Vec<u8>,
|
||||
#[br(count = 4)] version_bytes: Vec<u8>,
|
||||
data_size: u32,
|
||||
#[br(count = 20)] _unk2: Vec<u8>,
|
||||
dual_ruf_flag: u32,
|
||||
#[br(count = 44)] _unk3: Vec<u8>,
|
||||
payload_count: u16,
|
||||
_payload_entry_size: u16,
|
||||
payloads_start_offset: u32,
|
||||
}
|
||||
impl RufHeader {
|
||||
fn date_time(&self) -> String {
|
||||
common::string_from_bytes(&self.date_time_bytes)
|
||||
}
|
||||
fn buyer(&self) -> String {
|
||||
common::string_from_bytes(&self.buyer_bytes)
|
||||
}
|
||||
fn model(&self) -> String {
|
||||
common::string_from_bytes(&self.model_bytes)
|
||||
}
|
||||
fn region_info(&self) -> String {
|
||||
common::string_from_bytes(&self.region_info_bytes)
|
||||
}
|
||||
fn is_dual_ruf(&self) -> bool {
|
||||
if self.dual_ruf_flag == 0x44 {true} else {false}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(BinRead)]
|
||||
struct RufEntry {
|
||||
#[br(count = 32)] _metadata: Vec<u8>,
|
||||
payload_type_bytes: u32,
|
||||
size: u32,
|
||||
_unk1: u32,
|
||||
#[br(count = 20)] _unk2: Vec<u8>,
|
||||
}
|
||||
impl RufEntry {
|
||||
fn payload_type(&self) -> &str {
|
||||
if self.payload_type_bytes == 1 {
|
||||
return "squashfs"
|
||||
} else if self.payload_type_bytes == 2 {
|
||||
return "cfe"
|
||||
} else if self.payload_type_bytes == 3 {
|
||||
return "vmlinuz"
|
||||
} else if self.payload_type_bytes == 4 {
|
||||
return "loader"
|
||||
} else if self.payload_type_bytes == 5 {
|
||||
return "splash"
|
||||
} else {
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_ruf_file(file: &File) -> bool {
|
||||
let header = common::read_file(&file, 0, 3).expect("Failed to read from file.");
|
||||
if header == b"RUF" {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn decrypt(encrypted_data: &[u8], key: &[u8; 16], iv: &[u8; 16]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
let mut data = encrypted_data.to_vec();
|
||||
let decryptor = Aes128CbcDec::new(key.into(), iv.into());
|
||||
let decrypted = decryptor.decrypt_padded_mut::<Pkcs7>(&mut data)
|
||||
.map_err(|e| format!("!!Decryption error!!: {:?}", e))?;
|
||||
|
||||
Ok(decrypted.to_vec())
|
||||
}
|
||||
|
||||
pub fn extract_ruf(mut file: &File, output_folder: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let header: RufHeader = file.read_be()?;
|
||||
if header.is_dual_ruf() {
|
||||
println!("\nDual RUF detected! Extracting 1st RUF...\n");
|
||||
actually_extract_ruf(&file, &format!("{}/RUF_1", output_folder), 0)?;
|
||||
println!("\nExtracting 2nd RUF...\n");
|
||||
actually_extract_ruf(&file, &format!("{}/RUF_2", output_folder), 41943088)?;
|
||||
} else {
|
||||
actually_extract_ruf(&file, &output_folder, 0)?;
|
||||
}
|
||||
|
||||
println!("\nExtraction finished!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn actually_extract_ruf(mut file: &File, output_folder: &str, start_offset: u64) -> Result<(), Box<dyn std::error::Error>> {
|
||||
file.seek(SeekFrom::Start(start_offset))?;
|
||||
let header: RufHeader = file.read_be()?;
|
||||
|
||||
println!("File info:\nBuyer: {} \nModel: {} \nRegion Info: {} \nDateTime: {}\nVersion:{:02x?} \nData Size: {} \nDual RUF: {}",
|
||||
header.buyer(), header.model(), header.region_info(), header.date_time(), header.version_bytes, header.data_size, header.is_dual_ruf());
|
||||
|
||||
println!("\nPayload count: {}", header.payload_count);
|
||||
file.seek(SeekFrom::Start(start_offset + header.payloads_start_offset as u64))?;
|
||||
|
||||
let mut entries: Vec<RufEntry> = Vec::new();
|
||||
|
||||
let mut vi = 0;
|
||||
for _i in 0..28 {
|
||||
if vi == header.payload_count {
|
||||
break
|
||||
}
|
||||
let entry: RufEntry = file.read_be()?;
|
||||
|
||||
if entry.payload_type_bytes == 0 && entry.size == 0 {
|
||||
continue
|
||||
} else {
|
||||
vi += 1
|
||||
}
|
||||
|
||||
println!("{}/{}: Type: {}({}), Size: {}",
|
||||
vi, header.payload_count, entry.payload_type_bytes, entry.payload_type(), entry.size);
|
||||
|
||||
entries.push(entry);
|
||||
}
|
||||
|
||||
let mut key: Option<&str> = None;
|
||||
let key_bytes;
|
||||
let iv_bytes = hex::decode("00000000000000000000000000000000")?.as_slice().try_into()?;
|
||||
|
||||
//find key
|
||||
for (prefix, value) in keys::RUF {
|
||||
if header.model().starts_with(prefix) {
|
||||
key = Some(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(k) = key {
|
||||
println!("\nKey: {}", k);
|
||||
key_bytes = hex::decode(k)?.as_slice().try_into()?;
|
||||
} else {
|
||||
println!("\nSorry, this firmware is not supported!");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
file.seek(SeekFrom::Start(start_offset + 2048))?;
|
||||
let encrypted_data = common::read_exact(&mut file, header.data_size as usize)?;
|
||||
println!("Decrypting data...");
|
||||
let decrypted_data = decrypt(&encrypted_data, &key_bytes, &iv_bytes)?;
|
||||
|
||||
let mut data_reader = Cursor::new(decrypted_data);
|
||||
|
||||
let mut ei = 1;
|
||||
for entry in entries {
|
||||
println!("\nEntry {}/{}: {} - {}, Size: {}",
|
||||
ei, header.payload_count, entry.payload_type_bytes, entry.payload_type(), entry.size);
|
||||
|
||||
let data = common::read_exact(&mut data_reader, entry.size as usize)?;
|
||||
|
||||
let output_path = Path::new(&output_folder).join(format!("{}_{}.bin", entry.payload_type_bytes, entry.payload_type()));
|
||||
fs::create_dir_all(&output_folder)?;
|
||||
let mut out_file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(output_path)?;
|
||||
|
||||
out_file.write_all(&data)?;
|
||||
|
||||
println!("- Saved file!");
|
||||
|
||||
ei += 1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+15
-10
@@ -11,16 +11,6 @@ pub static SAMSUNG: &[(&str, &str)] = &[
|
||||
("T-ECP", "3EF6067262CF0C678598BFF22169D1F1EA57C284"),
|
||||
];
|
||||
|
||||
//pfl upg keys
|
||||
pub static PFLUPG: &[(&str, &str)] = &[
|
||||
("q5551", "D7EE8ED11AC048ED225D4F5F53F8509E55C5D94256A703C79E4CA78AE93ECE1639FE466363AD962BD6D6DE0C46FD19F363687C1D8A21820740A6E7FF87F41C4900DCE1E26EC122E5D4DFA76BFC8F296816B8D0910325E9DC5CBCC9579CF15FC0253EF7CF4919B7613491A5D7BF75DC1888531C458967FD1CF64B33139550BAA5"),
|
||||
("qf1eu", "ACD684155C7CCCB04372A8808514489FA9EE75D305987D1337420241FDBE0AE1F7CDFBB931C9D56C91D36F2CE79D222695B484FF42BCA12CE362C7C9ABBDEEC8E5D6107FADCF2D4DA5DF0693E13ACE54A18AEB21C051F6B62C075A1791985547C1CFF4FB5B6EA7E0A9405A1B2BB71EB89A9B209E0F62BF9794D673179C0E60F1"),
|
||||
("q591e", "AFAF89062747CBB29343C4E4EA775E4CFDF5FAFCD92C9DD858A8725201BA54AB973BEFEE04EBF3046910FBBC78B10120AE16D80BA734931E97248BC6B1D4A909F087D37BC0A9C2210FF8A2BE44C00F31E4DD8713A364623637FF75EBBCE9D3A840DB67E0FA910F127F679496F6C21112E3E3AD4ACA459FDE1CC58E300682E6F9"),
|
||||
("q522e", "C41BE92C212BAC76B48261E2A1704028287DD7E121C11DA25F709E864FBDC1BD8C7F226F57605A4B42D768CDD629AF9E54011A0967AFC2826331406FB1E90321620738526EA0BEA59F1A0E612AE891C396112F13531F423DF02F94D1C871429549F4D5B30D9CA3EFDCC6D7A96849F7C1788DE8FAAEDD36560337008DF06D612F"),
|
||||
("q5481", "9E7DA389815251E82A84A1182807702E72A0B0E0FF707C8E73E2EA71F79D5FAAFFC6E0B90ED16E13A4289C78A7D3BDA90626162AAE169D7BE28D6A635585CC10639C4E312E288EB8F7C5A44518B7E8A26A45C5023C5078A972A4CC219CA020BAF524F7429257B7AD76E1B15390879064C6ED59CC1F20CC04EEC26C9CF7FC0727"),
|
||||
("q5431", "C5FD937B301A5CEF4B6C25F187728C99636515D058895FEA469496E2B24907FC7721648841F8AE4C618E215673D0C029752FA970B6F9A7F48C9331293D3B1D43E4DFC7B52914973642CD3E4EE0AD11F5254505038F95CACE0DF21FC769B34E134435D88AB617D2981F66EF45BBC7796CFB1086C5D5672E837204991FE53BC1CF"),
|
||||
];
|
||||
|
||||
//MSD10 keys
|
||||
//github.com/bugficks/msddecrypt
|
||||
//fw prefix, type, key
|
||||
@@ -43,6 +33,21 @@ pub static MSD11: &[(&str, &str)] = &[
|
||||
("T-NKL", "5bab1098dab48792619ebd63650d929f"), //2020
|
||||
];
|
||||
|
||||
//RUF keys
|
||||
pub static RUF: &[(&str, &str)] = &[
|
||||
("BD-P1600", "CB44682CFC0E0C50E1BEA74294ABBAE6"),
|
||||
];
|
||||
|
||||
//pfl upg keys
|
||||
pub static PFLUPG: &[(&str, &str)] = &[
|
||||
("q5551", "D7EE8ED11AC048ED225D4F5F53F8509E55C5D94256A703C79E4CA78AE93ECE1639FE466363AD962BD6D6DE0C46FD19F363687C1D8A21820740A6E7FF87F41C4900DCE1E26EC122E5D4DFA76BFC8F296816B8D0910325E9DC5CBCC9579CF15FC0253EF7CF4919B7613491A5D7BF75DC1888531C458967FD1CF64B33139550BAA5"),
|
||||
("qf1eu", "ACD684155C7CCCB04372A8808514489FA9EE75D305987D1337420241FDBE0AE1F7CDFBB931C9D56C91D36F2CE79D222695B484FF42BCA12CE362C7C9ABBDEEC8E5D6107FADCF2D4DA5DF0693E13ACE54A18AEB21C051F6B62C075A1791985547C1CFF4FB5B6EA7E0A9405A1B2BB71EB89A9B209E0F62BF9794D673179C0E60F1"),
|
||||
("q591e", "AFAF89062747CBB29343C4E4EA775E4CFDF5FAFCD92C9DD858A8725201BA54AB973BEFEE04EBF3046910FBBC78B10120AE16D80BA734931E97248BC6B1D4A909F087D37BC0A9C2210FF8A2BE44C00F31E4DD8713A364623637FF75EBBCE9D3A840DB67E0FA910F127F679496F6C21112E3E3AD4ACA459FDE1CC58E300682E6F9"),
|
||||
("q522e", "C41BE92C212BAC76B48261E2A1704028287DD7E121C11DA25F709E864FBDC1BD8C7F226F57605A4B42D768CDD629AF9E54011A0967AFC2826331406FB1E90321620738526EA0BEA59F1A0E612AE891C396112F13531F423DF02F94D1C871429549F4D5B30D9CA3EFDCC6D7A96849F7C1788DE8FAAEDD36560337008DF06D612F"),
|
||||
("q5481", "9E7DA389815251E82A84A1182807702E72A0B0E0FF707C8E73E2EA71F79D5FAAFFC6E0B90ED16E13A4289C78A7D3BDA90626162AAE169D7BE28D6A635585CC10639C4E312E288EB8F7C5A44518B7E8A26A45C5023C5078A972A4CC219CA020BAF524F7429257B7AD76E1B15390879064C6ED59CC1F20CC04EEC26C9CF7FC0727"),
|
||||
("q5431", "C5FD937B301A5CEF4B6C25F187728C99636515D058895FEA469496E2B24907FC7721648841F8AE4C618E215673D0C029752FA970B6F9A7F48C9331293D3B1D43E4DFC7B52914973642CD3E4EE0AD11F5254505038F95CACE0DF21FC769B34E134435D88AB617D2981F66EF45BBC7796CFB1086C5D5672E837204991FE53BC1CF"),
|
||||
];
|
||||
|
||||
//epk2 keys
|
||||
//github.com/openlgtv/epk2extract
|
||||
pub static EPK2: &[(&str, &str)] = &[
|
||||
|
||||
@@ -82,6 +82,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("EPK file detected!");
|
||||
formats::epk::extract_epk(&file, &output_path)?;
|
||||
}
|
||||
else if formats::ruf::is_ruf_file(&file) {
|
||||
println!("RUF file detected!");
|
||||
formats::ruf::extract_ruf(&file, &output_path)?;
|
||||
}
|
||||
else if formats::pfl_upg::is_pfl_upg_file(&file) {
|
||||
println!("PFL UPG file detected!");
|
||||
formats::pfl_upg::extract_pfl_upg(&file, &output_path)?;
|
||||
|
||||
Reference in New Issue
Block a user