1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
//! Integration tests for the RAR1 module.
//!
//! RAR1 (the 1995-1996 original Roshal Archive compression algorithm) has
//! no working end-to-end decoder in this build — see `src/rar1/mod.rs` for
//! the rationale. These tests therefore exercise:
//!
//! 1. The public-surface contract: name, encoder permanently unsupported,
//! decoder constructors, `unpack_size` plumbing, expected error
//! behaviour on real RAR1-shaped inputs.
//! 2. Streaming-trait conformance: empty calls don't error, `finish`
//! behaves correctly on freshly-constructed and consumed decoders,
//! `reset` returns the decoder to its initial state.
//! 3. Building-block reachability: the bit reader, Huffman decoder, LZSS
//! window, lookup tables, and offset history all carry their own
//! in-module unit tests; this file additionally pins down the
//! publicly-observable behaviour those building blocks support
//! through the [`Decoder`] / [`Encoder`] API.
//!
//! Canonical v0.3 port: every codec call returns `(Progress, Status)` and
//! the loop dispatches on `Status` rather than inferring from byte counts.
//!
//! Fixture famine: real RAR1 sample files are virtually non-existent on
//! the open internet in 2026. If a future contributor turns one up, embed
//! it as hex below and add a `decode_real_fixture` test pointing at it.
#![cfg(feature = "rar1")]
use compcol::rar1::{Decoder, Encoder, Rar1};
use compcol::{Algorithm, Decoder as _, Encoder as _, Error, Status};
// ─── algorithm identity ───────────────────────────────────────────────────
#[test]
fn name_is_rar1() {
assert_eq!(<Rar1 as Algorithm>::NAME, "rar1");
}
#[test]
fn algorithm_factory_produces_codec() {
let _enc = <Rar1 as Algorithm>::encoder();
let _dec = <Rar1 as Algorithm>::decoder();
}
#[test]
fn factory_returns_decoder() {
// The decoder factory just needs to compile-and-call cleanly — the
// returned decoder's `decode` will refuse real input but it must
// construct.
let mut d = <Rar1 as Algorithm>::decoder();
let mut out = [0u8; 1];
// Empty input is permitted as a no-op.
let (p, status) = d.decode(&[], &mut out).unwrap();
assert_eq!(p.consumed, 0);
assert_eq!(p.written, 0);
// Empty input is "feed me more" — not stream end.
assert!(matches!(status, Status::InputEmpty));
}
#[test]
fn factory_returns_encoder_that_errors() {
let mut e = <Rar1 as Algorithm>::encoder();
let mut out = [0u8; 1];
assert_eq!(e.encode(b"x", &mut out).unwrap_err(), Error::Unsupported);
}
// ─── encoder is permanently Unsupported ──────────────────────────────────
#[test]
fn encoder_encode_is_unsupported() {
let mut e = Encoder::new();
let mut out = [0u8; 16];
assert_eq!(
e.encode(b"hello world", &mut out).unwrap_err(),
Error::Unsupported
);
}
#[test]
fn encoder_finish_is_unsupported() {
let mut e = Encoder::new();
let mut out = [0u8; 16];
assert_eq!(e.finish(&mut out).unwrap_err(), Error::Unsupported);
}
#[test]
fn encoder_encode_with_empty_input_still_unsupported() {
// The encoder doesn't carve out an "empty input is OK" path; the whole
// surface is permanently disabled and we want callers to find out
// immediately.
let mut e = Encoder::new();
let mut out = [0u8; 16];
assert_eq!(e.encode(&[], &mut out).unwrap_err(), Error::Unsupported);
}
#[test]
fn encoder_reset_does_not_panic() {
let mut e = Encoder::new();
e.reset();
// Still unsupported after reset.
let mut out = [0u8; 16];
assert_eq!(e.encode(b"x", &mut out).unwrap_err(), Error::Unsupported);
}
// ─── decoder constructors ────────────────────────────────────────────────
#[test]
fn decoder_new_has_no_unpack_size() {
let d = Decoder::new();
assert_eq!(d.unpack_size(), None);
}
#[test]
fn decoder_with_unpack_size_records_value() {
let d = Decoder::with_unpack_size(4_321);
assert_eq!(d.unpack_size(), Some(4_321));
}
#[test]
fn decoder_with_unpack_size_zero_is_valid() {
// A zero unpack size is a legal "empty file" payload in RAR1 (it
// signals the entry exists but has no decompressed bytes). The
// constructor must accept it.
let d = Decoder::with_unpack_size(0);
assert_eq!(d.unpack_size(), Some(0));
}
#[test]
fn decoder_with_unpack_size_large() {
// A 4 GiB unpack size shouldn't overflow our internal counter.
let d = Decoder::with_unpack_size(u64::from(u32::MAX));
assert_eq!(d.unpack_size(), Some(u64::from(u32::MAX)));
}
// ─── decoder streaming-trait conformance ─────────────────────────────────
#[test]
fn decode_empty_input_is_noop() {
let mut d = Decoder::new();
let mut out = [0u8; 4];
let (p, status) = d.decode(&[], &mut out).unwrap();
assert_eq!(p.consumed, 0);
assert_eq!(p.written, 0);
// Empty input ⇒ codec is waiting for bytes, not at end-of-stream.
assert!(matches!(status, Status::InputEmpty));
}
#[test]
fn decode_empty_input_zero_output_is_noop() {
// The all-zero case is the lowest-energy stress test of the trait
// contract: no input, no output buffer, decoder shouldn't error.
let mut d = Decoder::new();
let mut out: [u8; 0] = [];
let (p, status) = d.decode(&[], &mut out).unwrap();
assert_eq!(p.consumed, 0);
assert_eq!(p.written, 0);
assert!(matches!(status, Status::InputEmpty));
}
#[test]
fn decode_nonempty_input_returns_unsupported() {
// The decoder has no static Huffman tables wired in (see module
// docs) so any real input must be refused immediately.
let mut d = Decoder::new();
let mut out = [0u8; 16];
assert_eq!(
d.decode(b"\xCA\xFE", &mut out).unwrap_err(),
Error::Unsupported
);
}
#[test]
fn decode_nonempty_input_with_unpack_size_still_unsupported() {
// Supplying the declared decompressed length does not change the
// verdict — the algorithm is structurally not yet implemented.
let mut d = Decoder::with_unpack_size(128);
let mut out = [0u8; 16];
assert_eq!(d.decode(b"\x01", &mut out).unwrap_err(), Error::Unsupported);
}
#[test]
fn finish_on_fresh_decoder_is_stream_end() {
let mut d = Decoder::new();
let mut out = [0u8; 4];
let (p, status) = d.finish(&mut out).unwrap();
assert!(matches!(status, Status::StreamEnd));
assert_eq!(p.consumed, 0);
assert_eq!(p.written, 0);
}
#[test]
fn finish_on_fresh_decoder_with_unpack_size_is_stream_end() {
// Even if we declared an unpack size, an unstarted decoder is
// trivially "done" — there is no in-flight data to flush.
let mut d = Decoder::with_unpack_size(100);
let mut out = [0u8; 4];
let (p, status) = d.finish(&mut out).unwrap();
assert!(matches!(status, Status::StreamEnd));
assert_eq!(p.written, 0);
}
#[test]
fn reset_returns_to_initial_state() {
let mut d = Decoder::with_unpack_size(42);
// Trying to decode a non-empty input puts the decoder into the
// "unsupported" state but `reset` should clear it back to fresh.
let mut out = [0u8; 4];
let _ = d.decode(&[0xFF], &mut out);
d.reset();
// After reset: no declared unpack_size, finish reports StreamEnd.
assert_eq!(d.unpack_size(), None);
let (_p, status) = d.finish(&mut out).unwrap();
assert!(matches!(status, Status::StreamEnd));
}
#[test]
fn skip_default_implementation_propagates_unsupported() {
// The default `Decoder::discard_output` implementation drives `decode`.
// For our stub, that means the first non-empty `decode` call errors
// out and `discard_output` should propagate that error rather than
// spinning.
let mut d = Decoder::new();
let result = d.discard_output(b"some-bytes", 100);
assert_eq!(result.unwrap_err(), Error::Unsupported);
}
#[test]
fn skip_with_empty_input_returns_zero_progress() {
let mut d = Decoder::new();
let (p, _status) = d.discard_output(&[], 100).unwrap();
// The default impl breaks out of its loop when both consumed and
// written stay zero. Skipping zero from empty input → zero progress.
assert_eq!(p.consumed, 0);
assert_eq!(p.written, 0);
}
// ─── decoder-as-trait-object ─────────────────────────────────────────────
#[cfg(feature = "factory")]
mod factory {
use compcol::Error;
use compcol::factory;
#[test]
fn lookup_rar1_encoder_and_decoder() {
assert!(factory::encoder_by_name("rar1").is_some());
assert!(factory::decoder_by_name("rar1").is_some());
}
#[test]
fn lookup_unknown() {
assert!(factory::encoder_by_name("not-a-real-rar1").is_none());
assert!(factory::decoder_by_name("not-a-real-rar1").is_none());
}
#[test]
fn names_contains_rar1() {
assert!(factory::names().contains(&"rar1"));
}
#[test]
fn boxed_encoder_is_unsupported() {
let mut e = factory::encoder_by_name("rar1").expect("rar1 is in the factory");
let mut out = [0u8; 4];
assert_eq!(e.encode(b"x", &mut out).unwrap_err(), Error::Unsupported);
}
#[test]
fn boxed_decoder_is_unsupported_on_real_input() {
let mut d = factory::decoder_by_name("rar1").expect("rar1 is in the factory");
let mut out = [0u8; 4];
// Same constraints apply via dyn dispatch.
assert_eq!(d.decode(b"x", &mut out).unwrap_err(), Error::Unsupported);
}
}