From 036dc80ad0e50fb0e37ca357ed20f74b57f26915 Mon Sep 17 00:00:00 2001 From: Paul-Christian Volkmer Date: Sat, 30 Dec 2023 07:15:09 +0100 Subject: [PATCH] feat #24: List content of OSB files --- Cargo.lock | 1 + Cargo.toml | 1 + src/file_io.rs | 193 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 154 +++++++++++++++++++++------------------ 4 files changed, 277 insertions(+), 72 deletions(-) create mode 100644 src/file_io.rs diff --git a/Cargo.lock b/Cargo.lock index 6b59f5f..59195ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -478,6 +478,7 @@ dependencies = [ name = "osc-variant" version = "0.7.0" dependencies = [ + "bytes", "clap", "clap_complete", "console", diff --git a/Cargo.toml b/Cargo.toml index 1dd46ff..7b1f5f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ indicatif = "0.17" deob = { path = "./libs/deob", optional = true } zip = { version = "0.6", optional = true } +bytes = "1.5.0" [features] # Requires env var OSB_KEY to be present at build time diff --git a/src/file_io.rs b/src/file_io.rs new file mode 100644 index 0000000..f017104 --- /dev/null +++ b/src/file_io.rs @@ -0,0 +1,193 @@ +/* + * MIT License + * + * Copyright (c) 2023 Comprehensive Cancer Center Mainfranken + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +use crate::file_io::InputFile::{Osc, Other}; +use crate::model::onkostar_editor::OnkostarEditor; +use bytes::BytesMut; +use deob::deobfuscate; +use std::error::Error; +use std::fmt::{Debug, Display, Formatter}; +use std::fs; +use std::io::Read; +use std::path::Path; +use std::str::FromStr; + +pub enum FileError { + Reading(String, String), + Writing(String, String), + Parsing(String, String), +} + +impl Error for FileError {} + +impl Debug for FileError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(self, f) + } +} + +impl Display for FileError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match &self { + FileError::Reading(filename, err) => format!("Kann Datei '{}' nicht lesen: {}", filename, err), + FileError::Writing(filename, err) => format!("Kann Datei '{}' nicht schreiben: {}", filename, err), + FileError::Parsing(filename, err) => format!( + "Die Datei '{}' ist entweder keine OSB- oder OSC-Datei, fehlerhaft oder enthält zusätzliche Inhalte\n{}", + filename, + err + ), + } + ) + } +} + +#[allow(dead_code)] +pub enum InputFile { + Osc { + filename: String, + content: String, + }, + Profile { + filename: String, + content: String, + }, + Osb { + filename: String, + content: Vec, + }, + Other { + filename: String, + content: Vec, + }, +} + +impl InputFile { + pub fn filename(&self) -> String { + match self { + Osc { filename, .. } => filename, + InputFile::Profile { filename, .. } => filename, + InputFile::Osb { filename, .. } => filename, + Other { filename, .. } => filename, + } + .to_string() + } + + pub fn read(filename: String, password: Option) -> Result { + if let Some(extension) = Path::new(filename.as_str()).extension() { + return match extension.to_str() { + Some("osc") => match fs::read_to_string(filename.clone()) { + Ok(content) => Ok(Osc { filename, content }), + Err(err) => Err(FileError::Reading(filename, err.to_string())), + }, + #[cfg(feature = "unzip-osb")] + Some("osb") => { + let file = match fs::File::open(filename.clone()) { + Ok(file) => file, + Err(err) => return Err(FileError::Reading(filename, err.to_string())), + }; + + let mut archive = match zip::ZipArchive::new(file) { + Ok(file) => file, + Err(err) => return Err(FileError::Reading(filename, err.to_string())), + }; + + let mut result = vec![]; + + let password = password.unwrap_or_else(|| { + #[cfg(feature = "unzip-osb")] + { + deobfuscate(env!("OSB_KEY").trim()) + } + #[cfg(not(feature = "unzip-osb"))] + { + return Err(FileError::Reading(filename.clone(), "No Password".into())); + } + }); + + for i in 0..archive.len() { + if let Ok(Ok(mut zip_file)) = + archive.by_index_decrypt(i, password.as_bytes()) + { + if zip_file.is_file() && zip_file.name().ends_with(".osc") { + let mut buf = String::new(); + let _ = zip_file.read_to_string(&mut buf); + result.push(Osc { + filename: zip_file.name().to_string(), + content: buf, + }) + } else { + let mut buf = BytesMut::new(); + let _ = zip_file.read(&mut buf); + result.push(Other { + filename: zip_file.name().to_string(), + content: buf.to_vec(), + }) + } + } else { + return Err(FileError::Parsing( + filename.into(), + "Kann OSB-Datei nicht lesen".to_string(), + )); + } + } + + Ok(InputFile::Osb { + filename, + content: result, + }) + } + _ => Err(FileError::Parsing( + filename, + "Nur OSB- oder OSC-Dateien werden unterstützt".to_string(), + )), + }; + } + + Err(FileError::Reading(filename, String::new())) + } +} + +impl TryFrom for OnkostarEditor { + type Error = FileError; + + fn try_from(value: InputFile) -> Result { + return match value { + Osc { + filename, content, .. + } => match OnkostarEditor::from_str(content.as_str()) { + Ok(data) => Ok(data), + Err(err) => Err(FileError::Parsing(filename, err)), + }, + InputFile::Osb { filename, .. } + | InputFile::Profile { filename, .. } + | Other { filename, .. } => Err(FileError::Parsing( + filename, + "Nur OSC-Dateien werden unterstützt".to_string(), + )), + }; + } +} diff --git a/src/main.rs b/src/main.rs index 400449d..11ecdf1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,7 +23,6 @@ */ use std::error::Error; -use std::fmt::{Debug, Display, Formatter}; use std::fs; use std::fs::OpenOptions; use std::io::Write; @@ -40,66 +39,18 @@ use sha256::digest; use crate::checks::{check_file, print_checks, CheckNotice}; use crate::cli::{Cli, SubCommand}; +use crate::file_io::{FileError, InputFile}; use crate::model::onkostar_editor::OnkostarEditor; use crate::profile::Profile; mod checks; mod cli; +mod file_io; mod model; mod profile; #[cfg(feature = "unzip-osb")] mod unzip_osb; -enum FileError { - Reading(String, String), - Writing(String, String), - Parsing(String, String), -} - -impl Error for FileError {} - -impl Debug for FileError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - Display::fmt(self, f) - } -} - -impl Display for FileError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match &self { - FileError::Reading(filename, err) => format!("Kann Datei '{}' nicht lesen: {}", filename, err), - FileError::Writing(filename, err) => format!("Kann Datei '{}' nicht schreiben: {}", filename, err), - FileError::Parsing(filename, err) => format!( - "Die Datei '{}' ist entweder keine OSC-Datei, fehlerhaft oder enthält zusätzliche Inhalte\n{}", - filename, - err - ), - } - ) - } -} - -fn read_inputfile(inputfile: String) -> Result { - let filename = Path::new(&inputfile); - - if let Some(extension) = filename.extension() { - if let Some("osc") = extension.to_ascii_lowercase().to_str() { - return match fs::read_to_string(filename) { - Ok(content) => match OnkostarEditor::from_str(content.as_str()) { - Ok(data) => Ok(data), - Err(err) => Err(FileError::Parsing(inputfile, err)), - }, - Err(err) => Err(FileError::Reading(inputfile, err.to_string())), - }; - } - } - - Err(FileError::Reading(inputfile, "Keine OSC-Datei".into())) -} - fn write_outputfile(filename: String, content: &String) -> Result<(), FileError> { OpenOptions::new() .read(false) @@ -129,32 +80,89 @@ fn main() -> Result<(), Box> { inputfile, sorted, filter, - } => { - let mut data = read_inputfile(inputfile)?; - if sorted { - data.sorted() + } => match InputFile::read(inputfile, None)? { + osc @ InputFile::Osc { .. } => { + let mut content: OnkostarEditor = osc.try_into()?; + if sorted { + content.sorted() + } + if let Some(name) = filter { + OnkostarEditor::print_list_filtered(&mut content, name.as_str()); + return Ok(()); + } + content.print_list(); } - if let Some(name) = filter { - OnkostarEditor::print_list_filtered(&mut data, name.as_str()); - return Ok(()); + InputFile::Osb { content, .. } => { + for file in content { + match file { + InputFile::Osc { .. } => { + println!( + "{}{}", + style("OSB-Paketinhalt: ").bold().yellow(), + style(format!("{}", file.filename())).bold() + ); + + let mut content: OnkostarEditor = match file.try_into() { + Ok(oe) => oe, + Err(err) => { + println!("{}", err); + continue; + } + }; + + if sorted { + content.sorted() + } + if let Some(name) = filter { + OnkostarEditor::print_list_filtered(&mut content, name.as_str()); + return Ok(()); + } + content.print_list(); + println!() + } + _ => { + println!( + "{}{}{}", + style("OSB-Paketinhalt: ").bold().yellow(), + style(format!("{}", file.filename())).bold(), + style(" ignoriert").yellow() + ); + } + } + } } - data.print_list(); - } + InputFile::Profile { filename, .. } | InputFile::Other { filename, .. } => { + return Err(Box::new(FileError::Reading( + filename, + "Nur OSB- und OSC-Dateien werden unterstützt".to_string(), + ))) + } + }, SubCommand::Tree { inputfile, sorted, filter, - } => { - let mut data = read_inputfile(inputfile)?; - if sorted { - data.sorted() + } => match InputFile::read(inputfile, None)? { + osc @ InputFile::Osc { .. } => { + let mut content: OnkostarEditor = osc.try_into()?; + if sorted { + content.sorted() + } + if let Some(name) = filter { + OnkostarEditor::print_tree_filtered(&mut content, name.as_str()); + return Ok(()); + } + OnkostarEditor::print_tree(&content); } - if let Some(name) = filter { - OnkostarEditor::print_tree_filtered(&mut data, name.as_str()); - return Ok(()); + InputFile::Osb { filename, .. } + | InputFile::Profile { filename, .. } + | InputFile::Other { filename, .. } => { + return Err(Box::new(FileError::Reading( + filename, + "Nur OSC-Dateien werden unterstützt".to_string(), + ))) } - OnkostarEditor::print_tree(&data); - } + }, SubCommand::Modify { inputfile, profile, @@ -165,7 +173,7 @@ fn main() -> Result<(), Box> { interactive, fix, } => { - let data = &mut read_inputfile(inputfile)?; + let mut data: OnkostarEditor = InputFile::read(inputfile, None)?.try_into()?; if let Some(profile) = profile { let profile = if profile.contains('.') { @@ -254,8 +262,10 @@ fn main() -> Result<(), Box> { style(&inputfile_b).yellow() ); - let data_a = &mut read_inputfile(inputfile_a)?; - let data_b = &mut read_inputfile(inputfile_b)?; + let data_a: &mut OnkostarEditor = + &mut InputFile::read(inputfile_a, None)?.try_into()?; + let data_b: &mut OnkostarEditor = + &mut InputFile::read(inputfile_b, None)?.try_into()?; data_a.print_diff(data_b, strict); }