use std::sync::Arc;
use thiserror::Error;
use crate::util::Utf16String;
pub enum TokenStringResult<'a> {
Null,
Borrowed(&'a Utf16String),
Owned(Utf16String),
}
pub trait TokenValue {
fn token_to_string(&self) -> Result<TokenStringResult<'_>, TokenError>;
}
impl TokenValue for Utf16String {
fn token_to_string(&self) -> Result<TokenStringResult<'_>, TokenError> {
Ok(TokenStringResult::Borrowed(self))
}
}
impl<T: TokenValue> TokenValue for Arc<T> {
fn token_to_string(&self) -> Result<TokenStringResult<'_>, TokenError> {
self.as_ref().token_to_string()
}
}
#[derive(Debug, Error, Eq, PartialEq)]
pub enum TokenError {
#[error("")]
NullPointer,
#[error("String index out of range: {position}")]
StringIndexOutOfBounds {
position: i32,
},
#[error("{message}")]
Runtime {
exception_class_name: String,
message: String,
},
}
impl TokenError {
#[must_use]
pub fn class_name(&self) -> &str {
match self {
Self::NullPointer => "java.lang.NullPointerException",
Self::StringIndexOutOfBounds { .. } => "java.lang.StringIndexOutOfBoundsException",
Self::Runtime {
exception_class_name,
..
} => exception_class_name,
}
}
#[must_use]
pub fn runtime(exception_class_name: impl Into<String>, message: impl Into<String>) -> Self {
Self::Runtime {
exception_class_name: exception_class_name.into(),
message: message.into(),
}
}
}
pub struct Token<T: TokenValue> {
value: Option<T>,
}
impl<T: TokenValue> Token<T> {
#[must_use]
pub const fn new(value: Option<T>) -> Self {
Self { value }
}
#[must_use]
pub const fn get_value(&self) -> Option<&T> {
self.value.as_ref()
}
pub fn get_string_representation(&self) -> Result<TokenStringResult<'_>, TokenError> {
self.value
.as_ref()
.ok_or(TokenError::NullPointer)?
.token_to_string()
}
pub fn to_string(&self) -> Result<TokenStringResult<'_>, TokenError> {
self.get_string_representation()
}
pub fn is_token_char(context: Option<&Utf16String>, pos: i32) -> Result<bool, TokenError> {
let context = context.ok_or(TokenError::NullPointer)?;
let position = position_in(context, pos)?;
Ok(is_token_char_at(context.as_utf16(), position))
}
}
pub struct TokenParsingTracer {
_private: (),
}
impl TokenParsingTracer {
pub const TOKEN_SUBSTITUTE: u16 = 0x0023;
pub fn trace(input: Option<&Utf16String>) -> Result<Utf16String, TokenError> {
let input = input.ok_or(TokenError::NullPointer)?;
let input_units = input.as_utf16();
let mut traced = Vec::with_capacity(input_units.len().saturating_add(1));
for position in 0..input_units.len() {
if is_token_char_at(input_units, position) {
traced.push(Self::TOKEN_SUBSTITUTE);
} else {
traced.push(input_units[position]);
}
}
Ok(Utf16String::from_utf16(traced))
}
}
fn position_in(context: &Utf16String, position: i32) -> Result<usize, TokenError> {
let Ok(position_usize) = usize::try_from(position) else {
return Err(TokenError::StringIndexOutOfBounds { position });
};
if position_usize >= context.len() {
return Err(TokenError::StringIndexOutOfBounds { position });
}
Ok(position_usize)
}
fn is_token_char_at(context: &[u16], position: usize) -> bool {
let current = context[position];
if is_ascii_lower(current) || is_ascii_upper(current) || is_ascii_digit(current) {
return true;
}
if matches!(
current,
0x0020
| 0x000A
| 0x0028
| 0x0029
| 0x0027
| 0x0022
| 0x003C
| 0x003E
| 0x007B
| 0x007D
| 0x003D
| 0x002C
| 0x003B
| 0x003A
| 0x002B
| 0x002A
| 0x0024
| 0x0025
| 0x0026
| 0x0023
) {
return false;
}
if matches!(current, 0x005B | 0x005D | 0x002E | 0x005F) {
return true;
}
if current == u16::from(b'-') {
for index in (0..position).rev() {
if !is_token_char_at(context, index) {
break;
}
let candidate = context[index];
if !is_ascii_digit(candidate) && candidate != u16::from(b'.') {
return true;
}
}
for index in position.saturating_add(1)..context.len() {
let candidate = context[index];
if candidate == u16::from(b'-') {
return true;
}
if !is_token_char_at(context, index) {
break;
}
if !is_ascii_digit(candidate) && candidate != u16::from(b'.') {
return true;
}
}
return false;
}
current == 0x00B7
|| (0x00C0..=0x00D6).contains(¤t)
|| (0x00D8..=0x00F6).contains(¤t)
|| (0x00F8..=0x02FF).contains(¤t)
|| (0x0300..=0x036F).contains(¤t)
|| (0x0370..=0x037D).contains(¤t)
|| (0x037F..=0x1FFF).contains(¤t)
|| (0x200C..=0x200D).contains(¤t)
|| (0x203F..=0x2040).contains(¤t)
|| (0x2070..=0x218F).contains(¤t)
|| (0x2C00..=0x2FEF).contains(¤t)
|| (0x3001..=0xD7FF).contains(¤t)
|| (0xF900..=0xFDCF).contains(¤t)
|| (0xFDF0..=0xFFFD).contains(¤t)
}
const fn is_ascii_lower(value: u16) -> bool {
value >= 0x0061 && value <= 0x007A
}
const fn is_ascii_upper(value: u16) -> bool {
value >= 0x0041 && value <= 0x005A
}
const fn is_ascii_digit(value: u16) -> bool {
value >= 0x0030 && value <= 0x0039
}
#[cfg(test)]
mod tests {
use super::{Token, TokenError, TokenParsingTracer, TokenStringResult, TokenValue};
use crate::util::Utf16String;
struct Probe {
result: ProbeResult,
}
enum ProbeResult {
Null,
Value(Utf16String),
Error,
}
impl TokenValue for Probe {
fn token_to_string(&self) -> Result<TokenStringResult<'_>, TokenError> {
match &self.result {
ProbeResult::Null => Ok(TokenStringResult::Null),
ProbeResult::Value(value) => Ok(TokenStringResult::Borrowed(value)),
ProbeResult::Error => Err(TokenError::runtime(
"java.lang.IllegalStateException",
"boom",
)),
}
}
}
#[test]
fn preserves_value_identity_nullable_string_and_runtime_errors() {
let string = Utf16String::from_rust_str("token");
let token = Token::new(Some(string.clone()));
assert_eq!(token.get_value(), Some(&string));
assert!(matches!(
token.get_string_representation(),
Ok(TokenStringResult::Borrowed(value)) if value == &string
));
let null_token = Token::<Utf16String>::new(None);
assert_eq!(
null_token.get_string_representation().err(),
Some(TokenError::NullPointer)
);
let null_result = Token::new(Some(Probe {
result: ProbeResult::Null,
}));
assert_eq!(
std::mem::discriminant(&null_result.get_string_representation().unwrap()),
std::mem::discriminant(&TokenStringResult::Null)
);
let owned_result = Token::new(Some(Probe {
result: ProbeResult::Value(Utf16String::from_rust_str("owned")),
}));
let borrowed_result = owned_result.get_string_representation().unwrap();
let expected_borrowed_value = Utf16String::from_rust_str("expected");
assert_eq!(
std::mem::discriminant(&borrowed_result),
std::mem::discriminant(&TokenStringResult::Borrowed(&expected_borrowed_value))
);
let error_token = Token::new(Some(Probe {
result: ProbeResult::Error,
}));
let error = error_token
.get_string_representation()
.err()
.expect("runtime error");
assert_eq!(error.class_name(), "java.lang.IllegalStateException");
assert_eq!(error.to_string(), "boom");
}
#[test]
fn preserves_null_index_and_trace_boundaries() {
assert_eq!(
Token::<Utf16String>::is_token_char(None, 0).err(),
Some(TokenError::NullPointer)
);
let empty = Utf16String::from_rust_str("");
for position in [-1, 0, i32::MAX] {
let error = Token::<Utf16String>::is_token_char(Some(&empty), position)
.expect_err("index failure");
assert_eq!(
error.class_name(),
"java.lang.StringIndexOutOfBoundsException"
);
}
assert_eq!(
TokenParsingTracer::trace(None).err(),
Some(TokenError::NullPointer)
);
assert_eq!(
TokenParsingTracer::trace(Some(&empty))
.expect("empty trace")
.as_utf16(),
&[] as &[u16]
);
}
}