Skip to content

Commit 58ae20e

Browse files
committed
feat: Migrate Error Handling to thiserror
1 parent 0930a8c commit 58ae20e

10 files changed

Lines changed: 148 additions & 151 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ usvg = "0.47.0"
2323
# Video support (optional) — builds FFmpeg from source, statically linked
2424
ffmpeg-next = { version = "7", features = ["build", "build-license-gpl", "build-lib-x264"], optional = true }
2525
nalgebra = { version = "0.33", optional = true }
26+
thiserror = "2"
2627

2728
[dev-dependencies]
2829
tempfile = "3"

src/common/ecc.rs

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,13 @@ pub fn total_encoded_bits(num_blocks: usize) -> usize {
3030
/// A 2-byte length header (big-endian u16) is prepended. The payload is
3131
/// zero-padded to fill the full capacity so that the encoded bit length
3232
/// is always `total_encoded_bits(num_blocks)`.
33-
pub fn encode(message: &[u8], num_blocks: usize) -> Result<Vec<bool>, String> {
33+
pub fn encode(message: &[u8], num_blocks: usize) -> Result<Vec<bool>, crate::common::WatermarkError> {
3434
let max_msg = max_message_bytes(num_blocks);
3535
if max_msg == 0 {
36-
return Err("Image too small: insufficient capacity".to_string());
36+
return Err(crate::common::WatermarkError::ImageTooSmall);
3737
}
3838
if message.len() > max_msg {
39-
return Err(format!(
40-
"Message too long: max {} bytes, got {} bytes",
41-
max_msg,
42-
message.len()
43-
));
39+
return Err(crate::common::WatermarkError::CapacityExceeded { max: max_msg, actual: message.len(), mode: "global-dwt" });
4440
}
4541

4642
let total_data_bits = num_blocks / REPETITION_FACTOR;
@@ -76,9 +72,9 @@ pub fn encode(message: &[u8], num_blocks: usize) -> Result<Vec<bool>, String> {
7672
///
7773
/// `bits` should contain the raw extracted bits (scrambled order already resolved).
7874
/// Returns the decoded message bytes.
79-
pub fn decode(bits: &[bool]) -> Result<Vec<u8>, String> {
75+
pub fn decode(bits: &[bool]) -> Result<Vec<u8>, crate::common::WatermarkError> {
8076
if bits.len() < REPETITION_FACTOR * 16 {
81-
return Err("Not enough data to decode (need at least 2 header bytes)".to_string());
77+
return Err(crate::common::WatermarkError::ExtractionCorrupt);
8278
}
8379

8480
// Majority vote to recover original bits
@@ -112,21 +108,17 @@ pub fn decode(bits: &[bool]) -> Result<Vec<u8>, String> {
112108

113109
// Read length header
114110
if bytes.len() < 2 {
115-
return Err("Decoded data too short".to_string());
111+
return Err(crate::common::WatermarkError::ExtractionCorrupt);
116112
}
117113
let msg_len = ((bytes[0] as u16) << 8) | (bytes[1] as u16);
118114
let msg_len = msg_len as usize;
119115

120116
if msg_len == 0 {
121-
return Err("Invalid message length: 0".to_string());
117+
return Err(crate::common::WatermarkError::ExtractionCorrupt);
122118
}
123119

124120
if 2 + msg_len > bytes.len() {
125-
return Err(format!(
126-
"Message length {} exceeds available data {}",
127-
msg_len,
128-
bytes.len() - 2
129-
));
121+
return Err(crate::common::WatermarkError::ExtractionCorrupt);
130122
}
131123

132124
Ok(bytes[2..2 + msg_len].to_vec())

src/common/engine.rs

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,54 @@ pub struct ExtractResult {
9999
///
100100
/// Each engine implements format-specific feature detection and embedding
101101
/// while sharing the common layer (ECC, scrambling, password hashing).
102+
use std::io;
103+
use thiserror::Error;
104+
105+
#[derive(Debug, Error)]
106+
pub enum WatermarkError {
107+
#[error(transparent)]
108+
Image(#[from] image::ImageError),
109+
110+
#[error(transparent)]
111+
Io(#[from] io::Error),
112+
113+
#[error("message exceeds capacity: {actual} bytes, max {max} bytes (mode: {mode})")]
114+
CapacityExceeded {
115+
max: usize,
116+
actual: usize,
117+
mode: &'static str,
118+
},
119+
120+
#[error("image too small for watermarking: insufficient DWT coefficients")]
121+
ImageTooSmall,
122+
123+
#[error("unsupported file format: .{extension}")]
124+
UnsupportedFormat { extension: String },
125+
126+
#[error("no qualifying SVG paths with >= {min_coords} coordinates")]
127+
NoQualifyingPaths { min_coords: usize },
128+
129+
#[error("extraction failed: corrupt or missing watermark data")]
130+
ExtractionCorrupt,
131+
132+
#[error("video support not enabled; rebuild with: cargo build --features video")]
133+
VideoNotEnabled,
134+
135+
#[error("video processing failed: {0}")]
136+
VideoProcessing(String),
137+
138+
}
139+
102140
pub trait WatermarkEngine {
103-
/// Embed a watermark message into a file.
141+
/// Embed a watermark into a file.
104142
fn embed(
105143
&self,
106144
input_path: &str,
107145
message: &str,
108146
password: &str,
109147
intensity: u8,
110148
output_path: &str,
111-
) -> Result<EmbedResult, String>;
149+
) -> Result<EmbedResult, WatermarkError>;
112150

113151
/// Dry run: compute embedding info without writing any file.
114152
fn dry_run(
@@ -118,8 +156,8 @@ pub trait WatermarkEngine {
118156
password: &str,
119157
intensity: u8,
120158
output_path: &str,
121-
) -> Result<EmbedInfo, String>;
159+
) -> Result<EmbedInfo, WatermarkError>;
122160

123161
/// Verify and extract a watermark from a file.
124-
fn verify(&self, input_path: &str, password: &str) -> Result<ExtractResult, String>;
162+
fn verify(&self, input_path: &str, password: &str) -> Result<ExtractResult, WatermarkError>;
125163
}

src/common/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ pub mod password;
44
pub mod scramble;
55
pub mod temp_input_for_inference;
66

7-
pub use engine::{EmbedInfo, EmbedResult, ExtractResult, WatermarkEngine};
7+
pub use engine::{EmbedInfo, EmbedResult, ExtractResult, WatermarkEngine, WatermarkError};
88
pub use password::password_to_seed;
99
pub use temp_input_for_inference::TempInputForInference;

src/main.rs

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::path::Path;
22

33
use clap::{CommandFactory, Parser, Subcommand};
4-
use infinishield::common::WatermarkEngine;
4+
use infinishield::common::{WatermarkEngine, WatermarkError};
55
use infinishield::raster::RasterEngine;
66
use infinishield::vector::VectorEngine;
77
#[cfg(feature = "video")]
@@ -81,7 +81,7 @@ enum Commands {
8181
}
8282

8383
/// Detect file format and return the appropriate engine.
84-
fn engine_for_file(path: &str) -> Result<Box<dyn WatermarkEngine>, String> {
84+
fn engine_for_file(path: &str) -> Result<Box<dyn WatermarkEngine>, infinishield::common::WatermarkError> {
8585
let ext = Path::new(path)
8686
.extension()
8787
.and_then(|e| e.to_str())
@@ -96,13 +96,8 @@ fn engine_for_file(path: &str) -> Result<Box<dyn WatermarkEngine>, String> {
9696
#[cfg(feature = "video")]
9797
"mp4" | "webm" | "mov" | "avi" | "mkv" => Ok(Box::new(VideoEngine)),
9898
#[cfg(not(feature = "video"))]
99-
"mp4" | "webm" | "mov" | "avi" | "mkv" => Err(
100-
"Video support not enabled. Rebuild with: cargo build --features video".to_string(),
101-
),
102-
_ => Err(format!(
103-
"Unsupported file format: .{}. Supported: jpg, jpeg, png, webp, bmp, tiff, gif, svg, mp4, webm, mov, avi, mkv",
104-
ext
105-
)),
99+
"mp4" | "webm" | "mov" | "avi" | "mkv" => Err(WatermarkError::VideoNotEnabled.into()),
100+
_ => Err(WatermarkError::UnsupportedFormat { extension: ext.to_string() }.into()),
106101
}
107102
}
108103

@@ -180,7 +175,7 @@ fn main() {
180175
}
181176
Err(e) => {
182177
eprintln!("[错误] {}", e);
183-
std::process::exit(1);
178+
std::process::exit(1);
184179
}
185180
}
186181
} else {
@@ -190,7 +185,7 @@ fn main() {
190185
}
191186
Err(e) => {
192187
eprintln!("[错误] {}", e);
193-
std::process::exit(1);
188+
std::process::exit(1);
194189
}
195190
}
196191
}
@@ -227,4 +222,4 @@ fn main() {
227222
}
228223
}
229224
}
230-
}
225+
}

src/raster/mod.rs

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ pub mod features;
33

44
use crate::common::engine::{EmbedInfo, EmbedResult, ExtractResult, WatermarkEngine};
55
use crate::common::temp_input_for_inference::TempInputForInference;
6-
use crate::common::{ecc, password, scramble};
6+
use crate::common::{WatermarkError, ecc, password, scramble};
77

88
use features::{detect_keypoints, FeaturePoint, PATCH_SIZE};
99
use image::{DynamicImage, GenericImageView, GrayImage, Luma};
@@ -57,7 +57,7 @@ fn analyze(
5757
message: &str,
5858
intensity: u8,
5959
output_path: &str,
60-
) -> Result<(EmbedInfo, bool), String> {
60+
) -> Result<(EmbedInfo, bool), crate::common::WatermarkError> {
6161
let (w, h) = img.dimensions();
6262
let intensity = resolve_intensity(intensity, w, h);
6363
let gray = channel_to_gray(img);
@@ -100,8 +100,8 @@ impl WatermarkEngine for RasterEngine {
100100
password: &str,
101101
intensity: u8,
102102
output_path: &str,
103-
) -> Result<EmbedResult, String> {
104-
let img = image::open(input_path).map_err(|e| format!("Failed to open image: {}", e))?;
103+
) -> Result<EmbedResult, WatermarkError> {
104+
let img = image::open(input_path)?;
105105
let (info, use_fp) = analyze(&img, message, intensity, output_path)?;
106106
let gray = channel_to_gray(&img);
107107
let kps = detect_keypoints(&gray, MAX_KEYPOINTS);
@@ -125,20 +125,17 @@ impl WatermarkEngine for RasterEngine {
125125
_password: &str,
126126
intensity: u8,
127127
output_path: &str,
128-
) -> Result<EmbedInfo, String> {
129-
let img = image::open(input_path).map_err(|e| format!("Failed to open image: {}", e))?;
128+
) -> Result<EmbedInfo, WatermarkError> {
129+
let img = image::open(input_path)?;
130130
let (info, _) = analyze(&img, message, intensity, output_path)?;
131131
if info.message_bytes > info.max_capacity {
132-
return Err(format!(
133-
"Message too long: {} bytes, max capacity: {} bytes (mode: {})",
134-
info.message_bytes, info.max_capacity, info.mode
135-
));
132+
return Err(WatermarkError::CapacityExceeded { max: info.max_capacity, actual: info.message_bytes, mode: "global-dwt" });
136133
}
137134
Ok(info)
138135
}
139136

140-
fn verify(&self, input_path: &str, password: &str) -> Result<ExtractResult, String> {
141-
let img = image::open(input_path).map_err(|e| format!("Failed to open image: {}", e))?;
137+
fn verify(&self, input_path: &str, password: &str) -> Result<ExtractResult, WatermarkError> {
138+
let img = image::open(input_path)?;
142139
let gray = channel_to_gray(&img);
143140
let kps = detect_keypoints(&gray, MAX_KEYPOINTS);
144141
let channel = extract_channel(&img);
@@ -170,7 +167,7 @@ impl RasterEngine {
170167
message: &str,
171168
password: &str,
172169
intensity: u8,
173-
) -> Result<(), String> {
170+
) -> Result<(), WatermarkError> {
174171
let gray = gray_from_rgb(rgb, width, height, DETECT_CHANNEL);
175172
let kps = detect_keypoints(&gray, MAX_KEYPOINTS);
176173
let channel = channel_from_rgb(rgb, width, height, EMBED_CHANNEL);
@@ -222,7 +219,7 @@ impl RasterEngine {
222219
let mut coeffs = dwt::forward(&channel);
223220
let (br, bc, nb) = count_blocks(coeffs.hl.len(), coeffs.hl[0].len());
224221
if nb == 0 {
225-
return Err("Image too small for watermarking".to_string());
222+
return Err(WatermarkError::ImageTooSmall);
226223
}
227224
let bits = ecc::encode(message.as_bytes(), nb)?;
228225
let seed = password::password_to_seed(password);
@@ -267,7 +264,7 @@ impl RasterEngine {
267264
width: u32,
268265
height: u32,
269266
password: &str,
270-
) -> Result<ExtractResult, String> {
267+
) -> Result<ExtractResult, WatermarkError> {
271268
let gray = gray_from_rgb(rgb, width, height, DETECT_CHANNEL);
272269
let kps = detect_keypoints(&gray, MAX_KEYPOINTS);
273270
let channel = channel_from_rgb(rgb, width, height, EMBED_CHANNEL);
@@ -371,13 +368,9 @@ fn calculate_local_alpha(
371368
global_alpha * multiplier
372369
}
373370

374-
fn fp_encode(message: &[u8]) -> Result<Vec<bool>, String> {
371+
fn fp_encode(message: &[u8]) -> Result<Vec<bool>, WatermarkError> {
375372
if message.len() > FP_MAX_MESSAGE {
376-
return Err(format!(
377-
"Message too long for feature-point mode: max {} bytes, got {}",
378-
FP_MAX_MESSAGE,
379-
message.len()
380-
));
373+
return Err(WatermarkError::CapacityExceeded { max: FP_MAX_MESSAGE, actual: message.len(), mode: "feature-point" });
381374
}
382375
let total_bytes = BLOCKS_PER_PATCH / 8;
383376
let mut payload = vec![0u8; total_bytes];
@@ -393,9 +386,9 @@ fn fp_encode(message: &[u8]) -> Result<Vec<bool>, String> {
393386
Ok(bits)
394387
}
395388

396-
fn fp_decode(bits: &[bool]) -> Result<Vec<u8>, String> {
389+
fn fp_decode(bits: &[bool]) -> Result<Vec<u8>, WatermarkError> {
397390
if bits.len() < 8 {
398-
return Err("Not enough bits".to_string());
391+
return Err(WatermarkError::ExtractionCorrupt);
399392
}
400393
let mut bytes = Vec::with_capacity(bits.len() / 8);
401394
for chunk in bits.chunks(8) {
@@ -412,7 +405,7 @@ fn fp_decode(bits: &[bool]) -> Result<Vec<u8>, String> {
412405
}
413406
let len = bytes[0] as usize;
414407
if len == 0 || 1 + len > bytes.len() {
415-
return Err("Invalid message length".to_string());
408+
return Err(WatermarkError::ExtractionCorrupt);
416409
}
417410
Ok(bytes[1..1 + len].to_vec())
418411
}
@@ -424,7 +417,7 @@ fn embed_feature_point(
424417
password: &str,
425418
raw_intensity: u8,
426419
output_path: &str,
427-
) -> Result<(), String> {
420+
) -> Result<(), WatermarkError> {
428421
let (width, height) = img.dimensions();
429422
let alpha = fp_alpha(raw_intensity, width, height);
430423

@@ -478,7 +471,7 @@ fn verify_feature_point(
478471
keypoints: &[FeaturePoint],
479472
password: &str,
480473
channel: &[Vec<f64>],
481-
) -> Result<ExtractResult, String> {
474+
) -> Result<ExtractResult, WatermarkError> {
482475
let seed = password::password_to_seed(password);
483476
let ch_h = channel.len();
484477
let ch_w = if ch_h > 0 { channel[0].len() } else { 0 };
@@ -580,14 +573,14 @@ fn embed_global_dwt(
580573
password: &str,
581574
raw_intensity: u8,
582575
output_path: &str,
583-
) -> Result<(), String> {
576+
) -> Result<(), WatermarkError> {
584577
let (w, h) = img.dimensions();
585578
let alpha = dwt_alpha(raw_intensity, w, h);
586579
let ch = extract_channel(img);
587580
let mut coeffs = dwt::forward(&ch);
588581
let (br, bc, nb) = count_blocks(coeffs.hl.len(), coeffs.hl[0].len());
589582
if nb == 0 {
590-
return Err("Image too small for watermarking".to_string());
583+
return Err(WatermarkError::ImageTooSmall);
591584
}
592585

593586
let bits = ecc::encode(message.as_bytes(), nb)?;
@@ -629,7 +622,7 @@ fn embed_global_dwt(
629622
fn verify_global_dwt_from_channel(
630623
channel: &[Vec<f64>],
631624
password: &str,
632-
) -> Result<ExtractResult, String> {
625+
) -> Result<ExtractResult, WatermarkError> {
633626
let coeffs = dwt::forward(channel);
634627
let (_, bc, nb) = count_blocks(coeffs.hl.len(), coeffs.hl[0].len());
635628
if nb == 0 {
@@ -809,7 +802,7 @@ fn save_channel_to_image(
809802
width: u32,
810803
height: u32,
811804
path: &str,
812-
) -> Result<(), String> {
805+
) -> Result<(), WatermarkError> {
813806
let mut out = img.to_rgba8();
814807
let oh = height.min(channel.len() as u32);
815808
let ow = width.min(if channel.is_empty() {
@@ -825,8 +818,7 @@ fn save_channel_to_image(
825818
out.put_pixel(x, y, px);
826819
}
827820
}
828-
out.save(path)
829-
.map_err(|e| format!("Failed to save image: {}", e))
821+
Ok(out.save(path)?)
830822
}
831823

832824
#[cfg(test)]

0 commit comments

Comments
 (0)