1use std::fmt;
2use std::fs;
3use std::io::ErrorKind;
4use std::panic::{catch_unwind, AssertUnwindSafe};
5use std::path::{Path, PathBuf};
6
7use crate::contents::enums::{self, FileType};
8use crate::modules;
9
10#[derive(Debug, Clone)]
11pub struct CompressRequest {
12 pub file_type: FileType,
13 pub input: PathBuf,
14 pub output: PathBuf,
15}
16
17#[derive(Debug, Clone)]
18pub struct DecompressRequest {
19 pub input: PathBuf,
20 pub output: PathBuf,
21 pub level: i8,
22}
23
24#[derive(Debug, Clone)]
25pub struct OperationResult {
26 pub output_path: PathBuf,
27 pub message: String,
28}
29
30#[derive(Debug)]
31pub enum MagicPackError {
32 Io(std::io::Error),
33 UnsupportedFileType,
34 InvalidInput(String),
35 OperationFailed(String),
36}
37
38impl fmt::Display for MagicPackError {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 match self {
41 MagicPackError::Io(err) => write!(f, "{}", err),
42 MagicPackError::UnsupportedFileType => write!(f, "unsupported file type"),
43 MagicPackError::InvalidInput(message) => write!(f, "{}", message),
44 MagicPackError::OperationFailed(message) => write!(f, "{}", message),
45 }
46 }
47}
48
49impl std::error::Error for MagicPackError {}
50
51impl From<std::io::Error> for MagicPackError {
52 fn from(err: std::io::Error) -> Self {
53 match err.kind() {
54 ErrorKind::Unsupported => MagicPackError::UnsupportedFileType,
55 _ => MagicPackError::Io(err),
56 }
57 }
58}
59
60pub fn supported_formats() -> Vec<&'static str> {
61 vec!["zip", "tar", "bz2", "gz", "tar.bz2", "tar.gz"]
62}
63
64pub fn detect_file_type(path: &Path) -> Result<FileType, MagicPackError> {
65 modules::get_file_type(&path.to_path_buf()).map_err(MagicPackError::from)
66}
67
68pub fn compress(req: CompressRequest) -> Result<OperationResult, MagicPackError> {
69 validate_compress_request(&req)?;
70
71 let output_path = if req.output == Path::new(".") {
72 default_compress_output_path(&req.input, &req.output, req.file_type)?
73 } else {
74 req.output.clone()
75 };
76
77 run_operation("compress", || {
78 modules::compress(req.file_type, &req.input, &output_path);
79 })?;
80
81 Ok(OperationResult {
82 output_path,
83 message: format!(
84 "compressed as {}",
85 enums::get_file_type_string(req.file_type)
86 ),
87 })
88}
89
90pub fn decompress(req: DecompressRequest) -> Result<OperationResult, MagicPackError> {
91 validate_decompress_request(&req)?;
92
93 if req.output != Path::new(".") {
94 fs::create_dir_all(&req.output)?;
95 }
96
97 let src_filename = req.input.file_stem().ok_or_else(|| {
98 MagicPackError::InvalidInput("input path must include a file name".into())
99 })?;
100
101 let mut decompress_output = req.output.join(src_filename);
102 let mut decompress_input = req.input.clone();
103 let filename = decompress_output.file_name().ok_or_else(|| {
104 MagicPackError::InvalidInput("output path must include a file name".into())
105 })?;
106 let mg_filename = format!("mg_{}", filename.to_string_lossy());
107 decompress_output.set_file_name(mg_filename);
108
109 for index in 0..req.level {
110 let file_type = match detect_file_type(&decompress_input) {
111 Ok(file_type) => file_type,
112 Err(MagicPackError::UnsupportedFileType) if index != 0 => break,
113 Err(err) => return Err(err),
114 };
115
116 let current_output = decompress_output.clone();
117 run_operation("decompress", || {
118 modules::decompress(file_type, &decompress_input, ¤t_output);
119 })?;
120 decompress_input = current_output;
121 let temp_filename = decompress_input.file_stem().ok_or_else(|| {
122 MagicPackError::InvalidInput("decompressed output must include a file name".into())
123 })?;
124 decompress_output.set_file_name(temp_filename);
125 }
126
127 let final_filename = decompress_input
128 .file_name()
129 .ok_or_else(|| {
130 MagicPackError::InvalidInput("decompressed output must include a file name".into())
131 })?
132 .to_string_lossy()
133 .replace("mg_", "");
134 let mut final_output = decompress_input.clone();
135 final_output.set_file_name(final_filename);
136 fs::rename(&decompress_input, &final_output)?;
137
138 Ok(OperationResult {
139 output_path: final_output,
140 message: String::from("decompressed"),
141 })
142}
143
144fn validate_compress_request(req: &CompressRequest) -> Result<(), MagicPackError> {
145 if !req.input.exists() {
146 return Err(MagicPackError::InvalidInput(format!(
147 "input path does not exist: {}",
148 req.input.display()
149 )));
150 }
151
152 if req.output == Path::new(".") {
153 return Ok(());
154 }
155
156 if let Some(parent) = req.output.parent() {
157 if !parent.as_os_str().is_empty() {
158 fs::create_dir_all(parent)?;
159 }
160 }
161
162 Ok(())
163}
164
165fn validate_decompress_request(req: &DecompressRequest) -> Result<(), MagicPackError> {
166 if !req.input.exists() {
167 return Err(MagicPackError::InvalidInput(format!(
168 "input path does not exist: {}",
169 req.input.display()
170 )));
171 }
172
173 if req.level <= 0 {
174 return Err(MagicPackError::InvalidInput(
175 "decompress level must be greater than 0".into(),
176 ));
177 }
178
179 Ok(())
180}
181
182fn default_compress_output_path(
183 src_path: &Path,
184 dst_path: &Path,
185 file_type: FileType,
186) -> Result<PathBuf, MagicPackError> {
187 let filename = src_path.file_stem().ok_or_else(|| {
188 MagicPackError::InvalidInput("input path must include a file name".into())
189 })?;
190 let mut temp_output = dst_path.join(filename);
191 temp_output.set_extension(enums::get_file_type_string(file_type));
192 Ok(temp_output)
193}
194
195fn run_operation<F>(label: &str, operation: F) -> Result<(), MagicPackError>
196where
197 F: FnOnce(),
198{
199 catch_unwind(AssertUnwindSafe(operation)).map_err(|panic_payload| {
200 let message = if let Some(message) = panic_payload.downcast_ref::<&str>() {
201 (*message).to_string()
202 } else if let Some(message) = panic_payload.downcast_ref::<String>() {
203 message.clone()
204 } else {
205 format!("{} failed", label)
206 };
207 MagicPackError::OperationFailed(message)
208 })
209}