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 if rsiz & HTJ2K_RSIZ_MASK != 0 && self.htj2k.is_none() {
205 return Err(CapabilityMarkerError::Cap(
206 "SIZ advertises Part 15 without Pcap15",
207 ));
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 .as_chunks::<2>()
264 .0
265 .iter()
266 .map(|word| u16::from_be_bytes(*word)),
267 );
268 Ok(Self { words })
269 }
270}
271
272fn parse_cap(payload: &[u8]) -> Result<Option<Htj2kCapabilityCore>, CapabilityMarkerError> {
273 if payload.len() < 4 {
274 return Err(CapabilityMarkerError::Cap(
275 "CAP payload is shorter than Pcap",
276 ));
277 }
278 let pcap = u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]);
279 let expected_len = 4usize
280 .checked_add(pcap.count_ones() as usize * 2)
281 .ok_or(CapabilityMarkerError::Cap("CAP payload length overflows"))?;
282 if payload.len() != expected_len {
283 return Err(CapabilityMarkerError::Cap(
284 "CAP payload does not match the Pcap capability count",
285 ));
286 }
287 if pcap & HTJ2K_PCAP_MASK == 0 {
288 return Ok(None);
289 }
290 let preceding_capabilities = (pcap >> 18).count_ones() as usize;
291 let offset = 4 + preceding_capabilities * 2;
292 let ccap15 = u16::from_be_bytes([payload[offset], payload[offset + 1]]);
293 if ccap15 & CCAP15_RESERVED_MASK != 0 {
294 return Err(CapabilityMarkerError::Cap("CAP reserved Ccap15 bits"));
295 }
296 let mode = match ccap15 >> 14 {
297 0 => Htj2kCapabilityMode::HtOnly,
298 2 => Htj2kCapabilityMode::HtDeclared,
299 3 => Htj2kCapabilityMode::Mixed,
300 _ => return Err(CapabilityMarkerError::Cap("CAP reserved HT mode")),
301 };
302 Ok(Some(Htj2kCapabilityCore {
303 pcap,
304 ccap15,
305 mode,
306 multiple_ht_sets: ccap15 & (1 << 13) != 0,
307 roi: ccap15 & (1 << 12) != 0,
308 heterogeneous: ccap15 & (1 << 11) != 0,
309 ht_irreversible: ccap15 & (1 << 5) != 0,
310 magnitude_bound: decode_magnitude_bound((ccap15 & 0x1F) as u8),
311 }))
312}
313
314fn validate_cpf(payload: &[u8]) -> Result<(), CapabilityMarkerError> {
315 if payload.is_empty() || !payload.len().is_multiple_of(2) {
316 return Err(CapabilityMarkerError::Cpf(
317 "CPF payload must contain complete profile words",
318 ));
319 }
320 if payload[payload.len() - 2..] == [0, 0] {
321 return Err(CapabilityMarkerError::Cpf(
322 "CPF final profile word must be non-zero",
323 ));
324 }
325 Ok(())
326}
327
328fn profile_number_u64(word_count: usize, words: impl Iterator<Item = u16>) -> Option<u64> {
329 if word_count > 4 {
330 return None;
331 }
332 let mut encoded = 0u64;
333 for (index, word) in words.enumerate() {
334 encoded = encoded.checked_add(u64::from(word) << (index * 16))?;
335 }
336 encoded.checked_sub(1)
337}
338
339const fn decode_magnitude_bound(p: u8) -> u8 {
340 match p {
341 0 => 8,
342 1..=19 => p + 8,
343 20..=30 => 4 * (p - 19) + 27,
344 _ => 74,
345 }
346}