Skip to main content

sim_codec_classfile/
modified_utf8.rs

1//! Exact, strict JVM modified UTF-8 conversion.
2
3use sim_text::CodeUnitString;
4
5use crate::{ByteError, ByteErrorKind, ByteReader, ByteWriter};
6
7/// Decode strict JVM modified UTF-8 into exact UTF-16 code units.
8///
9/// Literal zero, overlong forms other than the required two-byte NUL, four-byte
10/// UTF-8, and unpaired surrogates are rejected. No scalar or lossy conversion occurs.
11pub fn decode_modified_utf8(
12    bytes: &[u8],
13    code_unit_budget: usize,
14) -> Result<CodeUnitString, ByteError> {
15    let mut reader = ByteReader::new(bytes, code_unit_budget);
16    let mut units = Vec::new();
17    let mut pending_high: Option<usize> = None;
18    while reader.remaining() != 0 {
19        let at = reader.offset();
20        let first = reader.read_u1()?;
21        let unit = match first {
22            0 => {
23                return Err(ByteError::new(
24                    ByteErrorKind::IllegalZero,
25                    at,
26                    "literal zero is illegal in modified UTF-8",
27                ));
28            }
29            1..=0x7f => u16::from(first),
30            0xc0..=0xdf => {
31                let second = continuation(&mut reader)?;
32                let value = (u16::from(first & 0x1f) << 6) | u16::from(second & 0x3f);
33                if value == 0 {
34                    if first != 0xc0 || second != 0x80 {
35                        return Err(ByteError::new(
36                            ByteErrorKind::OverlongModifiedUtf8,
37                            at,
38                            "non-canonical modified UTF-8 NUL",
39                        ));
40                    }
41                } else if value < 0x80 {
42                    return Err(ByteError::new(
43                        ByteErrorKind::OverlongModifiedUtf8,
44                        at,
45                        "overlong two-byte modified UTF-8",
46                    ));
47                }
48                value
49            }
50            0xe0..=0xef => {
51                let second = continuation(&mut reader)?;
52                let third = continuation(&mut reader)?;
53                let value = (u16::from(first & 0x0f) << 12)
54                    | (u16::from(second & 0x3f) << 6)
55                    | u16::from(third & 0x3f);
56                if value < 0x800 {
57                    return Err(ByteError::new(
58                        ByteErrorKind::OverlongModifiedUtf8,
59                        at,
60                        "overlong three-byte modified UTF-8",
61                    ));
62                }
63                value
64            }
65            _ => {
66                return Err(ByteError::new(
67                    ByteErrorKind::InvalidModifiedUtf8,
68                    at,
69                    "invalid modified UTF-8 lead byte",
70                ));
71            }
72        };
73
74        if let Some(high_at) = pending_high.take() {
75            if !(0xdc00..=0xdfff).contains(&unit) {
76                return Err(ByteError::new(
77                    ByteErrorKind::MalformedSurrogate,
78                    high_at,
79                    "high surrogate is not followed by a low surrogate",
80                ));
81            }
82        } else if (0xd800..=0xdbff).contains(&unit) {
83            pending_high = Some(at);
84        } else if (0xdc00..=0xdfff).contains(&unit) {
85            return Err(ByteError::new(
86                ByteErrorKind::MalformedSurrogate,
87                at,
88                "low surrogate has no preceding high surrogate",
89            ));
90        }
91        let next_len = units.len() + 1;
92        if next_len > code_unit_budget {
93            return Err(ByteError::new(
94                ByteErrorKind::BudgetExceeded,
95                at,
96                format!("code-unit length {next_len} exceeds budget {code_unit_budget}"),
97            ));
98        }
99        units.try_reserve_exact(1).map_err(|error| {
100            ByteError::new(
101                ByteErrorKind::BudgetExceeded,
102                at,
103                format!("code-unit allocation failed: {error}"),
104            )
105        })?;
106        units.push(unit);
107    }
108    if let Some(at) = pending_high {
109        return Err(ByteError::new(
110            ByteErrorKind::MalformedSurrogate,
111            at,
112            "high surrogate is truncated",
113        ));
114    }
115    Ok(CodeUnitString::try_from_code_units(units)
116        .expect("allocation preflight enforces the tighter caller budget"))
117}
118
119fn continuation(reader: &mut ByteReader<'_>) -> Result<u8, ByteError> {
120    let at = reader.offset();
121    let byte = reader.read_u1()?;
122    if byte & 0xc0 != 0x80 {
123        return Err(ByteError::new(
124            ByteErrorKind::InvalidModifiedUtf8,
125            at,
126            "invalid modified UTF-8 continuation byte",
127        ));
128    }
129    Ok(byte)
130}
131
132/// Encode exact, well-formed UTF-16 code units as strict JVM modified UTF-8.
133pub fn encode_modified_utf8(
134    text: &CodeUnitString,
135    byte_budget: usize,
136) -> Result<Vec<u8>, ByteError> {
137    let units = text.as_code_units();
138    validate_surrogates(units)?;
139    let required = units.iter().try_fold(0usize, |length, unit| {
140        let width = if *unit == 0 {
141            2
142        } else if *unit <= 0x7f {
143            1
144        } else if *unit <= 0x7ff {
145            2
146        } else {
147            3
148        };
149        length.checked_add(width).ok_or_else(|| {
150            ByteError::new(
151                ByteErrorKind::LengthOverflow,
152                length,
153                "modified UTF-8 output length overflow",
154            )
155        })
156    })?;
157    let mut writer = ByteWriter::new(byte_budget);
158    // One checked reservation/write per unit keeps the hard budget authoritative.
159    if required > byte_budget {
160        return Err(ByteError::new(
161            ByteErrorKind::BudgetExceeded,
162            0,
163            format!("modified UTF-8 output length {required} exceeds budget {byte_budget}"),
164        ));
165    }
166    for unit in units {
167        match *unit {
168            0 => writer.write_bytes(&[0xc0, 0x80])?,
169            1..=0x7f => writer.write_u1(*unit as u8)?,
170            0x80..=0x7ff => {
171                writer.write_bytes(&[0xc0 | (*unit >> 6) as u8, 0x80 | (*unit & 0x3f) as u8])?
172            }
173            _ => writer.write_bytes(&[
174                0xe0 | (*unit >> 12) as u8,
175                0x80 | ((*unit >> 6) & 0x3f) as u8,
176                0x80 | (*unit & 0x3f) as u8,
177            ])?,
178        }
179    }
180    Ok(writer.into_bytes())
181}
182
183fn validate_surrogates(units: &[u16]) -> Result<(), ByteError> {
184    let mut index = 0;
185    while index < units.len() {
186        match units[index] {
187            0xd800..=0xdbff
188                if units
189                    .get(index + 1)
190                    .is_some_and(|next| (0xdc00..=0xdfff).contains(next)) =>
191            {
192                index += 2
193            }
194            0xd800..=0xdfff => {
195                return Err(ByteError::new(
196                    ByteErrorKind::MalformedSurrogate,
197                    index,
198                    "unpaired surrogate code unit",
199                ));
200            }
201            _ => index += 1,
202        }
203    }
204    Ok(())
205}