mod amount;
mod currency;
mod dimensions;
mod encoding;
mod entry;
mod ids;
mod label;
mod path;
mod policy;
mod record;
pub use amount::{Amount, AmountError, Balance, Rounding};
pub use currency::{
AUD, BHD, BRL, CAD, CHF, CNY, CZK, Currency, DKK, EUR, GBP, HKD, HUF, INR, JPY, KRW, KWD, MXN,
NOK, NZD, PLN, SEK, SGD, TRY, USD, ZAR,
};
pub use dimensions::Dimensions;
pub use encoding::{DecodeError, MAX_LEAF_BYTES};
pub use entry::{
AccountRef, BuildError, CaptureTime, Document, Entry, Field, Fingerprint, HoldAction, Layer,
MemoCommitment, MemoSalt, PostAmount, Posting, Rate, Side,
};
pub use ids::{AccountHandle, EntryId, IdempotencyKey, LedgerId};
pub use label::{Label, PeriodId};
pub use path::AccountPath;
pub use policy::LedgerPolicy;
pub use record::{
AccountAction, AccountChange, AccountEvent, AccountKind, AccountRegistration,
BALANCE_LEAF_KIND, BalanceLimit, ClearingAction, ClearingEvent, CurrencyDeclaration,
EntryParts, EntryRecord, FORMAT, Genesis, PeriodDefinition, PolicyEvent, PostingRecord,
PostingRef, Record, RecordKind, Seal, SealOrderError, SealParts, TreeHeadEntry,
};
pub(crate) use encoding::{Decode, Encode, Reader, Sink, Writer, hex, pad};
pub(crate) use entry::record_fingerprint;
use unicode_normalization::UnicodeNormalization;
#[cfg(feature = "serde")]
pub(crate) mod serde_str {
use core::fmt::Display;
use core::str::FromStr;
pub(crate) fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: serde::Deserializer<'de>,
T: FromStr,
T::Err: Display,
{
let text = <std::borrow::Cow<'de, str> as serde::Deserialize>::deserialize(deserializer)?;
text.parse().map_err(serde::de::Error::custom)
}
}
#[cfg(feature = "serde")]
pub(crate) mod serde_hex {
pub(crate) fn serialize<S: serde::Serializer>(
bytes: &[u8],
serializer: S,
) -> Result<S::Ok, S::Error> {
struct Hex<'a>(&'a [u8]);
impl core::fmt::Display for Hex<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
super::hex::write(f, self.0)
}
}
serializer.collect_str(&Hex(bytes))
}
pub(crate) fn deserialize<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<u8>, D::Error> {
use serde::de::Error as _;
let text = <std::borrow::Cow<'de, str> as serde::Deserialize>::deserialize(deserializer)?;
super::hex::parse_vec(&text).ok_or_else(|| D::Error::custom(ValueError::InvalidHex))
}
use super::ValueError;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub enum ValueError {
#[error("the value is empty")]
Empty,
#[error("the value is too long: {found} exceeds the maximum of {max}")]
TooLong {
max: usize,
found: usize,
},
#[error("the text starts or ends with whitespace")]
EdgeWhitespace,
#[error("the text contains a control character")]
ControlCharacter,
#[error("the text contains an invisible character {found:?}")]
InvisibleCharacter {
found: char,
},
#[error("an account path segment contains the separator ':'")]
SeparatorInSegment,
#[error("the account path is deeper than {max} segments")]
TooDeep {
max: usize,
},
#[error("invalid character {found:?}")]
InvalidCharacter {
found: char,
},
#[error("the dimension axis already has a value")]
DuplicateAxis,
#[error("more than {} dimension axes", Dimensions::MAX_AXES)]
TooManyDimensions,
#[error("the value is out of range")]
OutOfRange,
#[error("the value must be positive")]
NotPositive,
#[error("a rate needs two different currencies")]
SameCurrency,
#[error("invalid hexadecimal text")]
InvalidHex,
#[error("the sequence is not in ascending order")]
NotSorted,
#[error("the sequence contains a duplicate")]
Duplicate,
#[error("currency exponent {exponent} exceeds the ledger scale {scale}")]
ExponentAboveScale {
exponent: u8,
scale: u8,
},
#[error("the record would encode to {found} bytes, more than the maximum of {max}")]
TooLarge {
max: usize,
found: usize,
},
}
impl From<core::convert::Infallible> for ValueError {
fn from(never: core::convert::Infallible) -> Self {
match never {}
}
}
pub(crate) const fn is_invisible(c: char) -> bool {
matches!(
c,
'\u{00AD}'
| '\u{034F}'
| '\u{061C}'
| '\u{115F}'..='\u{1160}'
| '\u{17B4}'..='\u{17B5}'
| '\u{180B}'..='\u{180F}'
| '\u{200B}'..='\u{200F}'
| '\u{2028}'..='\u{202E}'
| '\u{2060}'..='\u{206F}'
| '\u{3164}'
| '\u{FE00}'..='\u{FE0F}'
| '\u{FEFF}'
| '\u{FFA0}'
| '\u{FFF0}'..='\u{FFF8}'
| '\u{1BCA0}'..='\u{1BCA3}'
| '\u{1D173}'..='\u{1D17A}'
| '\u{E0000}'..='\u{E0FFF}'
)
}
pub(crate) fn normalize_label(text: &str, max_chars: usize) -> Result<String, ValueError> {
let normalized: String = text.nfc().collect();
if normalized.is_empty() {
return Err(ValueError::Empty);
}
let chars = normalized.chars().count();
if chars > max_chars {
return Err(ValueError::TooLong {
max: max_chars,
found: chars,
});
}
if normalized.starts_with(char::is_whitespace) || normalized.ends_with(char::is_whitespace) {
return Err(ValueError::EdgeWhitespace);
}
if normalized.chars().any(char::is_control) {
return Err(ValueError::ControlCharacter);
}
if let Some(found) = normalized.chars().find(|c| is_invisible(*c)) {
return Err(ValueError::InvisibleCharacter { found });
}
Ok(normalized)
}
pub(crate) fn is_nfc(text: &str) -> bool {
unicode_normalization::is_nfc(text)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nfd_text_is_normalised_to_nfc() {
assert_eq!(
normalize_label("Caf\u{65}\u{301}", 64).unwrap(),
"Caf\u{e9}"
);
assert!(is_nfc("Caf\u{e9}"));
assert!(!is_nfc("Caf\u{65}\u{301}"));
}
#[test]
fn label_rules_are_checked_in_order() {
assert_eq!(normalize_label("", 64), Err(ValueError::Empty));
assert_eq!(
normalize_label(&"a".repeat(64), 64).map(|s| s.len()),
Ok(64)
);
assert_eq!(
normalize_label(&"a".repeat(65), 64),
Err(ValueError::TooLong { max: 64, found: 65 })
);
assert_eq!(normalize_label(" a", 64), Err(ValueError::EdgeWhitespace));
assert_eq!(normalize_label("a ", 64), Err(ValueError::EdgeWhitespace));
assert_eq!(
normalize_label("a\u{7}b", 64),
Err(ValueError::ControlCharacter)
);
assert!(normalize_label(&"é".repeat(64), 64).is_ok());
for (text, found) in [
("Ca\u{200B}sh", '\u{200B}'),
("Cash\u{FEFF}", '\u{FEFF}'),
("Ca\u{200D}sh", '\u{200D}'),
("\u{202E}hsaC", '\u{202E}'),
("Ca\u{2028}sh", '\u{2028}'),
("Ca\u{00AD}sh", '\u{00AD}'),
("Ca\u{E0041}sh", '\u{E0041}'),
] {
assert_eq!(
normalize_label(text, 64),
Err(ValueError::InvisibleCharacter { found }),
"{text:?}"
);
}
}
}