1use core::marker::PhantomData;
2pub use hadris_common::types::{endian::*, number::*};
3
4#[cfg(feature = "std")]
5use std::time::SystemTime;
6
7#[cfg(feature = "alloc")]
8use alloc::vec::Vec;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum IsoStrError {
13 TooLong {
15 max: usize,
17 got: usize,
19 },
20 InvalidCharset,
22}
23
24impl core::fmt::Display for IsoStrError {
25 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
26 match self {
27 Self::TooLong { max, got } => {
28 write!(f, "string too long: max {max} bytes, got {got}")
29 }
30 Self::InvalidCharset => write!(f, "string contains invalid charset characters"),
31 }
32 }
33}
34
35pub trait Charset: Copy {
37 fn is_valid<'a>(bytes: impl Iterator<Item = &'a u8>) -> bool;
39 fn substitute_invalid<'a>(bytes: impl Iterator<Item = &'a mut u8>);
41}
42
43#[derive(Copy, Clone, PartialEq, Eq)]
46pub struct CharsetA;
47
48#[derive(Clone, Copy, PartialEq, Eq)]
49pub struct CharsetD;
51
52#[derive(Clone, Copy, PartialEq, Eq)]
53pub struct CharsetD1;
55
56impl CharsetA {
57 const VALID_SYMBOLS: &[u8] = b" 0123456789_!\"%$'()*+,-./:;<=>?";
58
59 fn valid_byte(b: u8) -> bool {
60 b.is_ascii_uppercase() || Self::VALID_SYMBOLS.contains(&b)
61 }
62}
63
64impl CharsetD {
65 const SPECIAL_CHARS: &[u8] = b"0123456789_";
66
67 fn valid_byte(b: u8) -> bool {
68 b.is_ascii_uppercase() || Self::SPECIAL_CHARS.contains(&b)
69 }
70}
71
72impl CharsetD1 {
73 const SPECIAL_CHARS: &[u8] = CharsetD::SPECIAL_CHARS;
74
75 fn valid_byte(b: u8) -> bool {
76 b.is_ascii_alphabetic() || Self::SPECIAL_CHARS.contains(&b)
77 }
78}
79
80impl Charset for CharsetA {
81 fn is_valid<'a>(mut bytes: impl Iterator<Item = &'a u8>) -> bool {
82 bytes.all(|b| Self::valid_byte(*b))
83 }
84
85 fn substitute_invalid<'a>(bytes: impl Iterator<Item = &'a mut u8>) {
86 for byte in bytes {
87 if byte.is_ascii_lowercase() {
88 *byte = byte.to_ascii_uppercase();
89 continue;
90 }
91
92 if !Self::valid_byte(*byte) {
93 *byte = b'_';
94 }
95 }
96 }
97}
98
99impl Charset for CharsetD {
100 fn is_valid<'a>(mut bytes: impl Iterator<Item = &'a u8>) -> bool {
101 bytes.all(|b| Self::valid_byte(*b))
102 }
103
104 fn substitute_invalid<'a>(bytes: impl Iterator<Item = &'a mut u8>) {
105 for byte in bytes {
106 if byte.is_ascii_lowercase() {
107 *byte = byte.to_ascii_uppercase();
108 continue;
109 }
110
111 if !Self::valid_byte(*byte) {
112 *byte = b'_';
113 }
114 }
115 }
116}
117
118impl Charset for CharsetD1 {
119 fn is_valid<'a>(mut bytes: impl Iterator<Item = &'a u8>) -> bool {
120 bytes.all(|b| Self::valid_byte(*b))
121 }
122
123 fn substitute_invalid<'a>(bytes: impl Iterator<Item = &'a mut u8>) {
124 for byte in bytes {
125 if !Self::valid_byte(*byte) {
126 *byte = b'_';
127 }
128 }
129 }
130}
131
132#[derive(Clone, Copy, PartialEq, Eq)]
134pub struct IsoStr<C: Charset, const N: usize> {
135 chars: [u8; N],
136 _marker: PhantomData<C>,
137}
138
139unsafe impl<C: Charset, const N: usize> bytemuck::Zeroable for IsoStr<C, N> {}
140unsafe impl<C: Charset + 'static, const N: usize> bytemuck::Pod for IsoStr<C, N> {}
141
142impl<C: Charset, const N: usize> IsoStr<C, N> {
143 pub fn empty() -> Self {
145 Self {
146 chars: [b' '; N],
147 _marker: core::marker::PhantomData,
148 }
149 }
150
151 pub fn max_len() -> usize {
153 N
154 }
155
156 pub fn len(&self) -> usize {
158 match self.chars.iter().rposition(|&c| c != b' ' && c != 0) {
159 Some(pos) => pos + 1,
160 None => 0,
161 }
162 }
163
164 pub fn is_empty(&self) -> bool {
166 self.len() == 0
167 }
168
169 pub fn as_bytes(&self) -> &[u8; N] {
171 &self.chars
172 }
173
174 pub const fn from_bytes_exact(bytes: [u8; N]) -> Self {
176 Self {
177 chars: bytes,
178 _marker: core::marker::PhantomData,
179 }
180 }
181
182 #[allow(clippy::should_implement_trait)]
183 pub fn from_str(s: &str) -> Result<Self, IsoStrError> {
185 let mut chars = [b' '; N];
186 if s.len() > N {
187 return Err(IsoStrError::TooLong {
188 max: N,
189 got: s.len(),
190 });
191 }
192
193 if !C::is_valid(s.as_bytes().iter()) {
194 return Err(IsoStrError::InvalidCharset);
195 }
196
197 for (i, c) in s.bytes().enumerate() {
198 chars[i] = c;
199 }
200 Ok(Self {
201 chars,
202 _marker: core::marker::PhantomData,
203 })
204 }
205
206 pub fn from_str_lossy(s: &str) -> Result<Self, IsoStrError> {
209 if s.len() > N {
210 return Err(IsoStrError::TooLong {
211 max: N,
212 got: s.len(),
213 });
214 }
215 let mut chars = [b' '; N];
216 for (i, c) in s.bytes().enumerate() {
217 chars[i] = c;
218 }
219 C::substitute_invalid(chars[..s.len()].iter_mut());
220 Ok(Self {
221 chars,
222 _marker: core::marker::PhantomData,
223 })
224 }
225
226 pub fn from_str_unchecked(s: &str) -> Result<Self, IsoStrError> {
229 if s.len() > N {
230 return Err(IsoStrError::TooLong {
231 max: N,
232 got: s.len(),
233 });
234 }
235 let mut chars = [b' '; N];
236 for (i, c) in s.bytes().enumerate() {
237 chars[i] = c;
238 }
239 Ok(Self {
240 chars,
241 _marker: core::marker::PhantomData,
242 })
243 }
244
245 pub fn to_str(&self) -> &str {
255 if self.chars.len() == 1 {
256 match self.chars[0] {
257 b'\x00' => return "\\x00",
258 b'\x01' => return "\\x01",
259 _ => {}
260 }
261 }
262 core::str::from_utf8(&self.chars[..self.len()]).expect("IsoStr contains invalid UTF-8")
263 }
264
265 pub fn try_to_str(&self) -> Result<&str, core::str::Utf8Error> {
267 if self.chars.len() == 1 {
268 match self.chars[0] {
269 b'\x00' => return Ok("\\x00"),
270 b'\x01' => return Ok("\\x01"),
271 _ => {}
272 }
273 }
274 core::str::from_utf8(&self.chars[..self.len()])
275 }
276}
277
278impl<C: Charset, const N: usize> core::fmt::Display for IsoStr<C, N> {
279 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
280 match self.try_to_str() {
281 Ok(s) => f.write_str(s),
282 Err(_) => write!(f, "{:?}", &self.chars[..self.len()]),
283 }
284 }
285}
286
287impl<C: Charset, const N: usize> core::fmt::Debug for IsoStr<C, N> {
288 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
289 match self.try_to_str() {
290 Ok(s) => write!(f, "\"{s}\""),
291 Err(_) => write!(f, "{:?}", &self.chars[..self.len()]),
292 }
293 }
294}
295
296#[cfg(feature = "alloc")]
298#[derive(Clone, PartialEq, Eq)]
299pub struct IsoString<C: Charset> {
300 chars: Vec<u8>,
301 _marker: PhantomData<C>,
302}
303
304#[cfg(feature = "alloc")]
305impl<C: Charset> From<Vec<u8>> for IsoString<C> {
306 fn from(value: Vec<u8>) -> Self {
307 if !(value.len() == 1 && (value[0] == 0x00 || value[0] == 0x01)) {
310 debug_assert!(
311 C::is_valid(value.iter()),
312 "IsoString contains invalid charset characters: {value:?}"
313 );
314 }
315 Self {
316 chars: value,
317 _marker: PhantomData,
318 }
319 }
320}
321
322#[cfg(feature = "alloc")]
323impl<C: Charset> IsoString<C> {
324 pub const fn empty() -> Self {
326 Self {
327 chars: Vec::new(),
328 _marker: PhantomData,
329 }
330 }
331
332 pub fn with_size(size: usize) -> Self {
334 use alloc::vec;
335 Self {
336 chars: vec![b' '; size],
338 _marker: PhantomData,
339 }
340 }
341
342 pub fn with_capacity(capacity: usize) -> Self {
344 Self {
345 chars: Vec::with_capacity(capacity),
346 _marker: PhantomData,
347 }
348 }
349
350 pub fn from_bytes(bytes: &[u8]) -> Self {
352 Self {
353 chars: bytes.to_vec(),
354 _marker: PhantomData,
355 }
356 }
357
358 pub fn from_utf8(str: &str) -> Self {
360 Self {
361 chars: str.as_bytes().to_vec(),
362 _marker: PhantomData,
363 }
364 }
365
366 pub fn len(&self) -> usize {
368 self.chars
369 .iter()
370 .position(|&c| c == b' ')
371 .unwrap_or(self.chars.len())
372 }
373
374 pub fn is_empty(&self) -> bool {
376 self.len() == 0
377 }
378
379 pub fn size(&self) -> usize {
381 self.chars.len()
382 }
383
384 pub fn bytes(&self) -> &[u8] {
386 &self.chars
387 }
388
389 pub fn as_str(&self) -> &str {
398 if self.chars.len() == 1 {
399 match self.chars[0] {
400 b'\x00' => return "\\x00",
401 b'\x01' => return "\\x01",
402 _ => {}
403 }
404 }
405 core::str::from_utf8(&self.chars[..self.len()]).expect("IsoString contains invalid UTF-8")
406 }
407
408 pub fn try_as_str(&self) -> Result<&str, core::str::Utf8Error> {
410 if self.chars.len() == 1 {
411 match self.chars[0] {
412 b'\x00' => return Ok("\\x00"),
413 b'\x01' => return Ok("\\x01"),
414 _ => {}
415 }
416 }
417 core::str::from_utf8(&self.chars[..self.len()])
418 }
419}
420
421#[cfg(feature = "alloc")]
422impl<C: Charset> core::fmt::Display for IsoString<C> {
423 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
424 match self.try_as_str() {
425 Ok(s) => f.write_str(s),
426 Err(_) => write!(f, "{:?}", &self.chars[..self.len()]),
427 }
428 }
429}
430
431#[cfg(feature = "alloc")]
432impl<C: Charset> core::fmt::Debug for IsoString<C> {
433 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
434 match self.try_as_str() {
435 Ok(s) => write!(f, "\"{s}\""),
436 Err(_) => write!(f, "{:?}", &self.chars[..self.len()]),
437 }
438 }
439}
440
441pub type IsoStrA<const N: usize> = IsoStr<CharsetA, N>;
443pub type IsoStrD<const N: usize> = IsoStr<CharsetD, N>;
445#[cfg(feature = "alloc")]
446pub type IsoStringA = IsoString<CharsetA>;
448#[cfg(feature = "alloc")]
449pub type IsoStringD = IsoString<CharsetD>;
451
452#[cfg(test)]
453mod iso_str_safety_tests {
454 use super::*;
455
456 #[test]
463 fn try_to_str_rejects_non_utf8_bytes() {
464 let mut bytes = [b' '; 16];
465 bytes[0] = 0xFF; let s: IsoStrA<16> = IsoStrA::from_bytes_exact(bytes);
467 assert!(s.try_to_str().is_err());
468 }
469
470 #[test]
471 #[should_panic(expected = "invalid UTF-8")]
472 fn to_str_panics_on_invalid_utf8() {
473 let mut bytes = [b' '; 16];
474 bytes[0] = 0xFF;
475 let s: IsoStrA<16> = IsoStrA::from_bytes_exact(bytes);
476 let _ = s.to_str();
477 }
478
479 #[test]
480 fn debug_does_not_panic_on_invalid_utf8() {
481 use core::fmt::Write as _;
482 struct Sink;
483 impl core::fmt::Write for Sink {
484 fn write_str(&mut self, _: &str) -> core::fmt::Result {
485 Ok(())
486 }
487 }
488 let mut bytes = [b' '; 16];
489 bytes[0] = 0xFF;
490 let s: IsoStrA<16> = IsoStrA::from_bytes_exact(bytes);
491 write!(Sink, "{s:?}").unwrap();
492 write!(Sink, "{s}").unwrap();
493 }
494
495 #[cfg(feature = "alloc")]
496 #[test]
497 fn iso_string_try_as_str_rejects_non_utf8() {
498 let s = IsoStringA::from_bytes(&[0xFFu8, 0xFE, b'a']);
499 assert!(s.try_as_str().is_err());
500 }
501
502 #[cfg(feature = "alloc")]
503 #[test]
504 fn iso_string_round_trips_valid_utf8() {
505 let s = IsoStringA::from_utf8("HELLO");
506 assert_eq!(s.try_as_str().unwrap(), "HELLO");
507 }
508}
509
510pub trait StdNum: Copy {
512 type LsbType: bytemuck::Pod + bytemuck::Zeroable + Endian<Output = Self>;
514 type MsbType: bytemuck::Pod + bytemuck::Zeroable + Endian<Output = Self>;
516}
517
518impl StdNum for u16 {
519 type LsbType = U16<LittleEndian>;
520 type MsbType = U16<BigEndian>;
521}
522
523impl StdNum for u32 {
524 type LsbType = U32<LittleEndian>;
525 type MsbType = U32<BigEndian>;
526}
527
528#[repr(C)]
529#[derive(Clone, Copy)]
530pub struct LsbMsb<T: StdNum> {
532 lsb: T::LsbType,
533 msb: T::MsbType,
534}
535
536impl<T> core::fmt::Debug for LsbMsb<T>
537where
538 T: StdNum + core::fmt::Debug,
539{
540 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
541 core::fmt::Debug::fmt(&self.read(), f)
542 }
543}
544
545unsafe impl<T: StdNum> bytemuck::Zeroable for LsbMsb<T> {}
546unsafe impl<T: StdNum + Copy + 'static> bytemuck::Pod for LsbMsb<T> {}
547
548impl<T: StdNum> LsbMsb<T> {
549 pub fn new(value: T) -> Self {
551 Self {
552 lsb: Endian::new(value),
553 msb: Endian::new(value),
554 }
555 }
556
557 pub fn read(&self) -> T {
559 #[cfg(target_endian = "little")]
560 {
561 self.lsb.get()
562 }
563 #[cfg(target_endian = "big")]
564 {
565 self.msb.get()
566 }
567 }
568
569 pub fn is_consistent(&self) -> bool
571 where
572 T: PartialEq,
573 {
574 self.lsb.get() == self.msb.get()
575 }
576
577 pub fn write(&mut self, value: T) {
579 self.lsb.set(value);
580 self.msb.set(value);
581 }
582}
583
584pub type U16LsbMsb = LsbMsb<u16>;
586pub type U32LsbMsb = LsbMsb<u32>;
588
589#[repr(C, packed)]
590#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
591pub struct DecDateTime {
593 pub year: IsoStrD<4>,
595 pub month: IsoStrD<2>,
597 pub day: IsoStrD<2>,
599 pub hour: IsoStrD<2>,
601 pub minute: IsoStrD<2>,
603 pub second: IsoStrD<2>,
605 pub hundredths: IsoStrD<2>,
607 pub timezone: u8,
609}
610
611impl core::fmt::Debug for DecDateTime {
612 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
613 f.debug_struct("DecDateTime")
614 .field("year", &self.year)
615 .field("month", &self.month)
616 .field("day", &self.day)
617 .field("hour", &self.hour)
618 .field("minute", &self.minute)
619 .field("second", &self.second)
620 .field("hundredths", &self.hundredths)
621 .field("timezone", &self.timezone)
622 .finish_non_exhaustive()
623 }
624}
625
626impl Default for DecDateTime {
627 fn default() -> Self {
628 Self {
629 year: IsoStrD::from_bytes_exact(*b"0000"),
630 month: IsoStrD::from_bytes_exact(*b"00"),
631 day: IsoStrD::from_bytes_exact(*b"00"),
632 hour: IsoStrD::from_bytes_exact(*b"00"),
633 minute: IsoStrD::from_bytes_exact(*b"00"),
634 second: IsoStrD::from_bytes_exact(*b"00"),
635 hundredths: IsoStrD::from_bytes_exact(*b"00"),
636 timezone: 0,
637 }
638 }
639}
640
641impl DecDateTime {
642 #[cfg(feature = "std")]
643 fn decimal<const N: usize>(mut value: u32) -> IsoStrD<N> {
644 let mut bytes = [b'0'; N];
645 for byte in bytes.iter_mut().rev() {
646 *byte = b'0' + (value % 10) as u8;
647 value /= 10;
648 }
649 IsoStrD::from_bytes_exact(bytes)
650 }
651
652 #[cfg(feature = "std")]
653 pub fn now() -> Self {
655 use chrono::{DateTime, Datelike, Timelike, Utc};
656 let now: DateTime<Utc> = SystemTime::now().into();
657 Self {
658 year: Self::decimal(now.year() as u32),
659 month: Self::decimal(now.month()),
660 day: Self::decimal(now.day()),
661 hour: Self::decimal(now.hour()),
662 minute: Self::decimal(now.minute()),
663 second: Self::decimal(now.second()),
664 hundredths: Self::decimal(now.nanosecond() / 10_000_000),
665 timezone: 0,
666 }
667 }
668
669 #[cfg(not(feature = "std"))]
671 pub fn now() -> Self {
672 Self::default()
673 }
674}
675
676#[cfg(all(test, feature = "std"))]
677mod tests {
678 use super::*;
679
680 #[test]
681 fn decimal_datetime_uses_zero_padded_digits() {
682 let date = DecDateTime::now();
683 for field in [
684 date.year.as_bytes().as_slice(),
685 date.month.as_bytes().as_slice(),
686 date.day.as_bytes().as_slice(),
687 date.hour.as_bytes().as_slice(),
688 date.minute.as_bytes().as_slice(),
689 date.second.as_bytes().as_slice(),
690 date.hundredths.as_bytes().as_slice(),
691 ] {
692 assert!(field.iter().all(u8::is_ascii_digit));
693 }
694 }
695
696 #[test]
697 fn test_charset_a_substitute() {
698 let original = b"thisisatest\\";
699 let mut new = original.to_vec();
700 CharsetA::substitute_invalid(new.iter_mut());
701 assert_eq!(new, b"THISISATEST_");
702 }
703
704 #[test]
705 fn test_charset_d_substitute() {
706 let original = b"thisisatest?new";
707 let mut new = original.to_vec();
708 CharsetD::substitute_invalid(new.iter_mut());
709 assert_eq!(new, b"THISISATEST_NEW");
710 }
711
712 #[test]
713 fn test_iso_str_len_trailing_spaces() {
714 let mut bytes = [b' '; 20];
717 bytes[..11].copy_from_slice(b"HELLO WORLD");
718 let s = IsoStrA::<20>::from_bytes_exact(bytes);
719 assert_eq!(s.len(), 11);
720 assert_eq!(s.to_str(), "HELLO WORLD");
721 }
722
723 #[test]
724 fn test_iso_str_len_all_spaces() {
725 let s = IsoStrA::<10>::empty();
726 assert_eq!(s.len(), 0);
727 assert!(s.is_empty());
728 }
729
730 #[test]
731 fn test_iso_str_len_no_trailing_spaces() {
732 let s = IsoStrA::<5>::from_str("ABCDE").unwrap();
733 assert_eq!(s.len(), 5);
734 }
735
736 #[test]
737 fn test_iso_str_from_str_lossy() {
738 let s = IsoStrA::<20>::from_str_lossy("hello world").unwrap();
740 assert_eq!(s.to_str(), "HELLO WORLD");
741
742 let s = IsoStrD::<10>::from_str_lossy("test\\new").unwrap();
744 assert_eq!(s.to_str(), "TEST_NEW");
745
746 let err = IsoStrA::<3>::from_str_lossy("toolong");
748 assert!(matches!(err, Err(IsoStrError::TooLong { max: 3, got: 7 })));
749 }
750
751 #[test]
752 fn test_iso_str_from_str_unchecked() {
753 let s = IsoStrA::<20>::from_str_unchecked("hello world").unwrap();
755 assert_eq!(s.to_str(), "hello world");
756
757 let s = IsoStrD::<10>::from_str_unchecked("test\\new").unwrap();
759 assert_eq!(s.to_str(), "test\\new");
760
761 let err = IsoStrA::<3>::from_str_unchecked("toolong");
763 assert!(matches!(err, Err(IsoStrError::TooLong { max: 3, got: 7 })));
764 }
765
766 #[test]
767 fn test_iso_str_len_embedded_spaces() {
768 let mut bytes = [b' '; 10];
770 bytes[..5].copy_from_slice(b"A B C");
771 let s = IsoStrA::<10>::from_bytes_exact(bytes);
772 assert_eq!(s.len(), 5);
773 assert_eq!(s.to_str(), "A B C");
774 }
775}