use crate::content::provider::SegmentProvider;
use crate::content::value::{BinaryValue, read_binary_value, read_string};
use crate::error::{Error, Result};
use crate::segment::record::RecordIdentifier;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[repr(u8)]
pub enum PropertyType {
String = 1,
Binary = 2,
Long = 3,
Double = 4,
Date = 5,
Boolean = 6,
Name = 7,
Path = 8,
Reference = 9,
WeakReference = 10,
Uri = 11,
Decimal = 12,
}
impl PropertyType {
#[must_use]
pub const fn from_tag(tag: u8) -> Option<Self> {
match tag {
1 => Some(Self::String),
2 => Some(Self::Binary),
3 => Some(Self::Long),
4 => Some(Self::Double),
5 => Some(Self::Date),
6 => Some(Self::Boolean),
7 => Some(Self::Name),
8 => Some(Self::Path),
9 => Some(Self::Reference),
10 => Some(Self::WeakReference),
11 => Some(Self::Uri),
12 => Some(Self::Decimal),
_ => None,
}
}
#[must_use]
pub const fn jcr_name(self) -> &'static str {
match self {
Self::String => "String",
Self::Binary => "Binary",
Self::Long => "Long",
Self::Double => "Double",
Self::Date => "Date",
Self::Boolean => "Boolean",
Self::Name => "Name",
Self::Path => "Path",
Self::Reference => "Reference",
Self::WeakReference => "WeakReference",
Self::Uri => "URI",
Self::Decimal => "Decimal",
}
}
}
#[derive(Clone, PartialEq, Debug)]
pub enum PropertyValue {
String(String),
Binary(BinaryValue),
Long(i64),
Double(f64),
Date(String),
Boolean(bool),
Name(String),
Path(String),
Reference(String),
WeakReference(String),
Uri(String),
Decimal(String),
}
impl PropertyValue {
#[must_use]
pub fn as_text(&self) -> Option<String> {
match self {
Self::String(text)
| Self::Date(text)
| Self::Name(text)
| Self::Path(text)
| Self::Reference(text)
| Self::WeakReference(text)
| Self::Uri(text)
| Self::Decimal(text) => Some(text.clone()),
Self::Long(value) => Some(value.to_string()),
Self::Double(value) => Some(double_to_text(*value)),
Self::Boolean(value) => Some(value.to_string()),
Self::Binary(_) => None,
}
}
}
#[must_use]
pub fn double_to_text(value: f64) -> String {
if value.is_nan() {
return "NaN".to_owned();
}
if value.is_infinite() {
return if value.is_sign_positive() {
"Infinity".to_owned()
} else {
"-Infinity".to_owned()
};
}
if value == 0.0 {
return if value.is_sign_negative() {
"-0.0".to_owned()
} else {
"0.0".to_owned()
};
}
let scientific = format!("{:e}", value.abs());
let Some((mantissa, exponent_text)) = scientific.split_once('e') else {
return value.to_string();
};
let Ok(exponent) = exponent_text.parse::<i32>() else {
return value.to_string();
};
let digits: String = mantissa
.chars()
.filter(|character| *character != '.')
.collect();
let sign = if value.is_sign_negative() { "-" } else { "" };
let digit_count = digits.len() as i32;
let mut rendered = String::new();
if (-3..7).contains(&exponent) {
if exponent < 0 {
rendered.push_str("0.");
for _ in 0..(-exponent - 1) {
rendered.push('0');
}
rendered.push_str(&digits);
} else if exponent >= digit_count - 1 {
rendered.push_str(&digits);
for _ in 0..(exponent - (digit_count - 1)) {
rendered.push('0');
}
rendered.push_str(".0");
} else {
let (integer_digits, fraction_digits) = digits.split_at(exponent as usize + 1);
rendered.push_str(integer_digits);
rendered.push('.');
rendered.push_str(fraction_digits);
}
} else {
let (first_digit, rest) = digits.split_at(1);
rendered.push_str(first_digit);
rendered.push('.');
if rest.is_empty() {
rendered.push('0');
} else {
rendered.push_str(rest);
}
rendered.push('E');
rendered.push_str(&exponent.to_string());
}
format!("{sign}{rendered}")
}
pub fn read_property_value(
provider: &dyn SegmentProvider,
value_identifier: RecordIdentifier,
property_type: PropertyType,
) -> Result<PropertyValue> {
if property_type == PropertyType::Binary {
return Ok(PropertyValue::Binary(read_binary_value(
provider,
value_identifier,
)?));
}
let text = read_string(provider, value_identifier)?;
let corrupt = |kind: &str| Error::InvalidFormat {
details: format!("stored {kind} value {text:?} at {value_identifier} cannot be decoded"),
};
Ok(match property_type {
PropertyType::String => PropertyValue::String(text),
PropertyType::Long => PropertyValue::Long(text.parse().map_err(|_| corrupt("long"))?),
PropertyType::Double => {
PropertyValue::Double(text.parse().map_err(|_| corrupt("double"))?)
}
PropertyType::Date => PropertyValue::Date(text),
PropertyType::Boolean => PropertyValue::Boolean(text.eq_ignore_ascii_case("true")),
PropertyType::Name => PropertyValue::Name(text),
PropertyType::Path => PropertyValue::Path(text),
PropertyType::Reference => PropertyValue::Reference(text),
PropertyType::WeakReference => PropertyValue::WeakReference(text),
PropertyType::Uri => PropertyValue::Uri(text),
PropertyType::Decimal => PropertyValue::Decimal(text),
PropertyType::Binary => unreachable!("handled above"),
})
}
#[cfg(test)]
mod tests {
use super::{PropertyType, PropertyValue, read_property_value};
use crate::content::provider::tests::MemorySegmentProvider;
use crate::segment::parsed_segment::tests::{data_segment_identifier, synthetic_data_segment};
use crate::segment::record::RecordIdentifier;
fn small_string_record(text: &str) -> Vec<u8> {
let mut bytes = vec![text.len() as u8];
bytes.extend_from_slice(text.as_bytes());
bytes
}
#[test]
fn property_type_tags_round_trip() {
for tag in 1..=12u8 {
let property_type = PropertyType::from_tag(tag).expect("valid tag");
assert_eq!(property_type as u8, tag);
}
assert_eq!(PropertyType::from_tag(0), None);
assert_eq!(PropertyType::from_tag(13), None);
}
#[test]
fn decodes_typed_values_from_stored_strings() {
let segment = data_segment_identifier(1);
let mut provider = MemorySegmentProvider::default();
provider.insert(
segment,
synthetic_data_segment(
&[],
&[
(0, 4, small_string_record("hello")),
(1, 4, small_string_record("-42")),
(2, 4, small_string_record("2.5")),
(3, 4, small_string_record("true")),
(4, 4, small_string_record("Infinity")),
(5, 4, small_string_record("2024-01-15T10:30:00.000Z")),
],
),
);
let record = |record_number| RecordIdentifier::new(segment, record_number);
assert_eq!(
read_property_value(&provider, record(0), PropertyType::String).expect("string"),
PropertyValue::String("hello".to_owned())
);
assert_eq!(
read_property_value(&provider, record(1), PropertyType::Long).expect("long"),
PropertyValue::Long(-42)
);
assert_eq!(
read_property_value(&provider, record(2), PropertyType::Double).expect("double"),
PropertyValue::Double(2.5)
);
assert_eq!(
read_property_value(&provider, record(3), PropertyType::Boolean).expect("boolean"),
PropertyValue::Boolean(true)
);
assert_eq!(
read_property_value(&provider, record(4), PropertyType::Double).expect("infinity"),
PropertyValue::Double(f64::INFINITY)
);
assert_eq!(
read_property_value(&provider, record(5), PropertyType::Date).expect("date"),
PropertyValue::Date("2024-01-15T10:30:00.000Z".to_owned())
);
}
#[test]
fn rejects_undecodable_stored_values() {
let segment = data_segment_identifier(1);
let mut provider = MemorySegmentProvider::default();
provider.insert(
segment,
synthetic_data_segment(&[], &[(0, 4, small_string_record("not-a-number"))]),
);
let record = RecordIdentifier::new(segment, 0);
assert!(read_property_value(&provider, record, PropertyType::Long).is_err());
assert_eq!(
read_property_value(&provider, record, PropertyType::Boolean).expect("boolean"),
PropertyValue::Boolean(false)
);
}
#[test]
fn booleans_decode_like_java_parse_boolean() {
let segment = data_segment_identifier(1);
let mut provider = MemorySegmentProvider::default();
provider.insert(
segment,
synthetic_data_segment(
&[],
&[
(0, 4, small_string_record("TRUE")),
(1, 8, small_string_record("false")),
],
),
);
assert_eq!(
read_property_value(
&provider,
RecordIdentifier::new(segment, 0),
PropertyType::Boolean
)
.expect("case-insensitive true"),
PropertyValue::Boolean(true)
);
assert_eq!(
read_property_value(
&provider,
RecordIdentifier::new(segment, 1),
PropertyType::Boolean
)
.expect("false"),
PropertyValue::Boolean(false)
);
}
#[test]
fn as_text_renders_scalar_values() {
assert_eq!(PropertyValue::Long(7).as_text(), Some("7".to_owned()));
assert_eq!(
PropertyValue::Boolean(false).as_text(),
Some("false".to_owned())
);
assert_eq!(
PropertyValue::Name("nt:file".to_owned()).as_text(),
Some("nt:file".to_owned())
);
}
#[test]
fn doubles_render_like_java_double_to_string() {
use super::double_to_text;
assert_eq!(double_to_text(0.0), "0.0");
assert_eq!(double_to_text(-0.0), "-0.0");
assert_eq!(double_to_text(1.0), "1.0");
assert_eq!(double_to_text(-1.5), "-1.5");
assert_eq!(double_to_text(100.0), "100.0");
assert_eq!(double_to_text(0.5), "0.5");
assert_eq!(double_to_text(12345.678), "12345.678");
assert_eq!(double_to_text(0.001), "0.001");
assert_eq!(double_to_text(0.0001), "1.0E-4");
assert_eq!(double_to_text(-0.000_025), "-2.5E-5");
assert_eq!(double_to_text(9_999_999.0), "9999999.0");
assert_eq!(double_to_text(10_000_000.0), "1.0E7");
assert_eq!(double_to_text(123_456_789.0), "1.23456789E8");
assert_eq!(
double_to_text(f64::MAX),
"1.7976931348623157E308",
"the maximum double keeps its full seventeen digits"
);
assert_eq!(double_to_text(f64::NAN), "NaN");
assert_eq!(double_to_text(f64::INFINITY), "Infinity");
assert_eq!(double_to_text(f64::NEG_INFINITY), "-Infinity");
for value in [
0.0,
-0.0,
1.0,
-1.5,
12345.678,
0.0001,
9_999_999.0,
1.0e300,
5e-324,
f64::MAX,
] {
let round_tripped: f64 = double_to_text(value).parse().expect("parses");
assert_eq!(
round_tripped.to_bits(),
value.to_bits(),
"{value} round-trips"
);
}
}
}