1use alloc::vec::Vec;
6
7mod magnitude;
8pub(crate) use magnitude::encode_magnitude_bound;
9pub use magnitude::required_magnitude_bound;
10
11const HTJ2K_PCAP_MASK: u32 = 1 << 17;
12pub(crate) const HTJ2K_RSIZ_MASK: u16 = 1 << 14;
13const CCAP15_RESERVED_MASK: u16 = 0x07C0;
14
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub enum Htj2kCapabilityMode {
18 HtOnly,
20 HtDeclared,
22 Mixed,
24}
25
26#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct J2kCorrespondingProfile {
29 words: Vec<u16>,
30}
31
32impl J2kCorrespondingProfile {
33 #[must_use]
35 pub fn words(&self) -> &[u16] {
36 &self.words
37 }
38
39 #[must_use]
41 pub fn number_u64(&self) -> Option<u64> {
42 profile_number_u64(self.words.len(), self.words.iter().copied())
43 }
44}
45
46#[derive(Clone, Debug, Eq, PartialEq)]
48#[expect(
49 clippy::struct_excessive_bools,
50 reason = "the booleans expose independent Part 15 CAP and COD capability facts"
51)]
52pub struct Htj2kCapabilities {
53 pcap: u32,
54 ccap15: u16,
55 mode: Htj2kCapabilityMode,
56 multiple_ht_sets: bool,
57 roi: bool,
58 heterogeneous: bool,
59 ht_irreversible: bool,
60 magnitude_bound: u8,
61 quality_layers: u8,
62 default_ht_block_coding: bool,
63 default_mixed_block_coding: bool,
64 corresponding_profile: Option<J2kCorrespondingProfile>,
65}
66
67impl Htj2kCapabilities {
68 #[must_use]
70 pub const fn pcap(&self) -> u32 {
71 self.pcap
72 }
73
74 #[must_use]
76 pub const fn ccap15(&self) -> u16 {
77 self.ccap15
78 }
79
80 #[must_use]
82 pub const fn mode(&self) -> Htj2kCapabilityMode {
83 self.mode
84 }
85
86 #[must_use]
88 pub const fn multiple_ht_sets(&self) -> bool {
89 self.multiple_ht_sets
90 }
91
92 #[must_use]
94 pub const fn roi(&self) -> bool {
95 self.roi
96 }
97
98 #[must_use]
100 pub const fn heterogeneous(&self) -> bool {
101 self.heterogeneous
102 }
103
104 #[must_use]
106 pub const fn ht_irreversible(&self) -> bool {
107 self.ht_irreversible
108 }
109
110 #[must_use]
112 pub const fn magnitude_bound(&self) -> u8 {
113 self.magnitude_bound
114 }
115
116 #[must_use]
118 pub const fn quality_layers(&self) -> u8 {
119 self.quality_layers
120 }
121
122 #[must_use]
124 pub const fn default_ht_block_coding(&self) -> bool {
125 self.default_ht_block_coding
126 }
127
128 #[must_use]
130 pub const fn default_mixed_block_coding(&self) -> bool {
131 self.default_mixed_block_coding
132 }
133
134 #[must_use]
136 pub const fn corresponding_profile(&self) -> Option<&J2kCorrespondingProfile> {
137 self.corresponding_profile.as_ref()
138 }
139}
140
141#[derive(Clone, Copy, Debug, Eq, PartialEq)]
142pub(crate) enum CapabilityMarkerError {
143 Cap(&'static str),
144 Cpf(&'static str),
145 Allocation { bytes: usize },
146}
147
148impl CapabilityMarkerError {
149 pub(crate) const fn marker_label(self) -> &'static str {
150 match self {
151 Self::Cap(what) | Self::Cpf(what) => what,
152 Self::Allocation { .. } => "CPF profile allocation",
153 }
154 }
155}
156
157#[derive(Clone, Copy, Debug)]
158#[expect(
159 clippy::struct_excessive_bools,
160 reason = "the booleans preserve independent validated Ccap15 flag bits"
161)]
162struct Htj2kCapabilityCore {
163 pcap: u32,
164 ccap15: u16,
165 mode: Htj2kCapabilityMode,
166 multiple_ht_sets: bool,
167 roi: bool,
168 heterogeneous: bool,
169 ht_irreversible: bool,
170 magnitude_bound: u8,
171}
172
173#[derive(Default)]
174pub(crate) struct CapabilityMarkerState<'a> {
175 saw_cap: bool,
176 htj2k: Option<Htj2kCapabilityCore>,
177 cpf_payload: Option<&'a [u8]>,
178}
179
180impl<'a> CapabilityMarkerState<'a> {
181 pub(crate) fn record_cap(&mut self, payload: &'a [u8]) -> Result<(), CapabilityMarkerError> {
182 if self.saw_cap {
183 return Err(CapabilityMarkerError::Cap("duplicate CAP"));
184 }
185 self.saw_cap = true;
186 self.htj2k = parse_cap(payload)?;
187 Ok(())
188 }
189
190 pub(crate) fn record_cpf(&mut self, payload: &'a [u8]) -> Result<(), CapabilityMarkerError> {
191 if self.cpf_payload.is_some() {
192 return Err(CapabilityMarkerError::Cpf("duplicate CPF"));
193 }
194 validate_cpf(payload)?;
195 self.cpf_payload = Some(payload);
196 Ok(())
197 }
198
199 pub(crate) fn validate_rsiz(&self, rsiz: u16) -> Result<(), CapabilityMarkerError> {
200 match (rsiz & HTJ2K_RSIZ_MASK != 0, self.htj2k.is_some()) {
201 (true, false) => Err(CapabilityMarkerError::Cap(
202 "SIZ advertises Part 15 without Pcap15",
203 )),
204 (false, true) => Err(CapabilityMarkerError::Cap(
205 "Pcap15 is present without the Part 15 SIZ capability",
206 )),
207 _ => Ok(()),
208 }?;
209 if self.cpf_payload.is_some() && self.htj2k.is_none() {
210 return Err(CapabilityMarkerError::Cpf(
211 "CPF is present without Part 15 capabilities",
212 ));
213 }
214 Ok(())
215 }
216
217 pub(crate) fn high_throughput(&self) -> bool {
218 self.htj2k.is_some()
219 }
220
221 pub(crate) fn to_public(
222 &self,
223 quality_layers: u8,
224 default_ht_block_coding: bool,
225 default_mixed_block_coding: bool,
226 ) -> Result<Option<Htj2kCapabilities>, CapabilityMarkerError> {
227 let Some(core) = self.htj2k else {
228 return Ok(None);
229 };
230 let corresponding_profile = self
231 .cpf_payload
232 .map(J2kCorrespondingProfile::from_payload)
233 .transpose()?;
234 Ok(Some(Htj2kCapabilities {
235 pcap: core.pcap,
236 ccap15: core.ccap15,
237 mode: core.mode,
238 multiple_ht_sets: core.multiple_ht_sets,
239 roi: core.roi,
240 heterogeneous: core.heterogeneous,
241 ht_irreversible: core.ht_irreversible,
242 magnitude_bound: core.magnitude_bound,
243 quality_layers,
244 default_ht_block_coding,
245 default_mixed_block_coding,
246 corresponding_profile,
247 }))
248 }
249}
250
251impl J2kCorrespondingProfile {
252 fn from_payload(payload: &[u8]) -> Result<Self, CapabilityMarkerError> {
253 let word_count = payload.len() / 2;
254 let bytes = word_count
255 .checked_mul(core::mem::size_of::<u16>())
256 .ok_or(CapabilityMarkerError::Allocation { bytes: usize::MAX })?;
257 let mut words = Vec::new();
258 words
259 .try_reserve_exact(word_count)
260 .map_err(|_| CapabilityMarkerError::Allocation { bytes })?;
261 words.extend(
262 payload
263 .chunks_exact(2)
264 .map(|word| u16::from_be_bytes([word[0], word[1]])),
265 );
266 Ok(Self { words })
267 }
268}
269
270fn parse_cap(payload: &[u8]) -> Result<Option<Htj2kCapabilityCore>, CapabilityMarkerError> {
271 if payload.len() < 4 {
272 return Err(CapabilityMarkerError::Cap(
273 "CAP payload is shorter than Pcap",
274 ));
275 }
276 let pcap = u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]);
277 let expected_len = 4usize
278 .checked_add(pcap.count_ones() as usize * 2)
279 .ok_or(CapabilityMarkerError::Cap("CAP payload length overflows"))?;
280 if payload.len() != expected_len {
281 return Err(CapabilityMarkerError::Cap(
282 "CAP payload does not match the Pcap capability count",
283 ));
284 }
285 if pcap & HTJ2K_PCAP_MASK == 0 {
286 return Ok(None);
287 }
288 let preceding_capabilities = (pcap >> 18).count_ones() as usize;
289 let offset = 4 + preceding_capabilities * 2;
290 let ccap15 = u16::from_be_bytes([payload[offset], payload[offset + 1]]);
291 if ccap15 & CCAP15_RESERVED_MASK != 0 {
292 return Err(CapabilityMarkerError::Cap("CAP reserved Ccap15 bits"));
293 }
294 let mode = match ccap15 >> 14 {
295 0 => Htj2kCapabilityMode::HtOnly,
296 2 => Htj2kCapabilityMode::HtDeclared,
297 3 => Htj2kCapabilityMode::Mixed,
298 _ => return Err(CapabilityMarkerError::Cap("CAP reserved HT mode")),
299 };
300 Ok(Some(Htj2kCapabilityCore {
301 pcap,
302 ccap15,
303 mode,
304 multiple_ht_sets: ccap15 & (1 << 13) != 0,
305 roi: ccap15 & (1 << 12) != 0,
306 heterogeneous: ccap15 & (1 << 11) != 0,
307 ht_irreversible: ccap15 & (1 << 5) != 0,
308 magnitude_bound: decode_magnitude_bound((ccap15 & 0x1F) as u8),
309 }))
310}
311
312fn validate_cpf(payload: &[u8]) -> Result<(), CapabilityMarkerError> {
313 if payload.is_empty() || !payload.len().is_multiple_of(2) {
314 return Err(CapabilityMarkerError::Cpf(
315 "CPF payload must contain complete profile words",
316 ));
317 }
318 if payload[payload.len() - 2..] == [0, 0] {
319 return Err(CapabilityMarkerError::Cpf(
320 "CPF final profile word must be non-zero",
321 ));
322 }
323 Ok(())
324}
325
326fn profile_number_u64(word_count: usize, words: impl Iterator<Item = u16>) -> Option<u64> {
327 if word_count > 4 {
328 return None;
329 }
330 let mut encoded = 0u64;
331 for (index, word) in words.enumerate() {
332 encoded = encoded.checked_add(u64::from(word) << (index * 16))?;
333 }
334 encoded.checked_sub(1)
335}
336
337const fn decode_magnitude_bound(p: u8) -> u8 {
338 match p {
339 0 => 8,
340 1..=19 => p + 8,
341 20..=30 => 4 * (p - 19) + 27,
342 _ => 74,
343 }
344}