1use std::{
2 convert::TryInto,
3 fs::File,
4 io::{self, prelude::*, Seek, SeekFrom},
5 num::NonZeroU64,
6 path::Path,
7 result::Result,
8};
9
10use deku::prelude::*;
11use itertools::Itertools;
12use memchr::memmem::find_iter;
13
14#[derive(thiserror::Error, Debug)]
15pub enum StrfileError {
16 #[error("failed to open file {0}")]
17 Open(io::Error),
18 #[error("failed to write datfile {0}")]
19 Write(DekuError),
20 #[error("failed to parse datfile {0}")]
21 DatParse(DekuError),
22 #[error("failed to get quote {0}")]
23 GetQuote(io::Error),
24 #[error("I/O Error {0}")]
25 Io(#[from] io::Error),
26 #[error("QuoteIndex")]
27 QuoteIndex,
28 #[error("Overflow")]
29 Overflow,
30 #[cfg(feature = "download")]
31 #[error("Download")]
32 Download(#[from] Box<ureq::Error>),
33}
34
35#[derive(Debug, Eq, PartialEq, DekuRead, DekuWrite, Default)]
36#[deku(endian = "big")]
37pub struct Datfile {
38 pub version: u32,
39 #[deku(update = "self.data.len()")]
40 pub count: u32,
41 pub max_length: u32,
42 pub min_length: u32,
43 pub flags: u32,
44 #[deku(pad_bytes_after = "3")]
45 pub delim: u8,
46 #[deku(count = "count")]
47 pub data: Vec<u64>,
48}
49
50impl Datfile {
51 pub const fn is_random(&self) -> bool {
52 (self.flags & 0x1) != 0
53 }
54
55 pub const fn is_ordered(&self) -> bool {
56 (self.flags & 0x2) != 0
57 }
58
59 pub const fn is_encrypted(&self) -> bool {
60 (self.flags & 0x4) != 0
61 }
62
63 pub fn get(&self, index: usize) -> Option<(u64, NonZeroU64)> {
64 let a = *self.data.get(index)?;
65 let b = if self.is_ordered() || self.is_random() {
66 self.data
67 .iter()
68 .filter_map(|i| i.checked_sub(2))
69 .filter(|i| *i > a)
70 .min()?
71 } else {
72 self.data.get(index + 1)?.checked_sub(2)?
73 };
74
75 Some((a, NonZeroU64::new(b)?))
76 }
77
78 pub fn build(strfile: &[u8], delim: u8, flags: u32) -> Self {
79 let mut min_length = u32::MAX;
80 let mut max_length = 0;
81
82 let data: Vec<u64> = std::iter::once(0_u64)
83 .chain(std::iter::once(0_u64))
84 .chain(
85 find_iter(strfile, &[b'\n', delim, b'\n'])
86 .filter_map(|i| i.try_into().ok())
87 .map(|i: u64| i + 3),
88 )
89 .tuple_windows()
90 .filter_map(|(a, b)| {
91 let diff = {
92 let t = b.checked_sub(a + 2)?;
93 u32::try_from(t).ok()?
94 };
95
96 if diff > max_length {
97 max_length = diff;
98 }
99 if diff < min_length {
100 min_length = diff;
101 }
102 Some(b)
103 })
104 .collect();
105
106 if max_length == u32::MAX {
107 max_length = 0;
108 }
109
110 Self {
111 version: 2,
112 count: data.len().try_into().unwrap_or(u32::MAX),
113 min_length,
114 max_length,
115 flags,
116 delim,
117 data,
118 }
119 }
120}
121
122pub struct Strfile {
123 file: File,
124 pub metadata: Datfile,
125}
126
127impl Strfile {
128 pub fn new(strfile: &Path, datfile: &Path) -> Result<Self, StrfileError> {
129 let mut datfile = File::open(datfile).map_err(StrfileError::Open)?;
130 let metadata = Datfile::from_reader((&mut datfile, 0))
131 .map_err(StrfileError::DatParse)?
132 .1;
133
134 let file = File::open(strfile).map_err(StrfileError::Open)?;
135
136 Ok(Self { file, metadata })
137 }
138
139 pub fn random_quote(&mut self) -> Result<String, StrfileError> {
140 self.get_quote(fastrand::usize(..self.metadata.count as usize - 1))
141 }
142
143 pub fn get_quote(&mut self, index: usize) -> Result<String, StrfileError> {
144 let (start, end) = self.metadata.get(index).ok_or(StrfileError::QuoteIndex)?;
145
146 self.file
147 .seek(SeekFrom::Start(start))
148 .map_err(StrfileError::GetQuote)?;
149 let buf_size = usize::try_from(end.get() - start).map_err(|_| StrfileError::Overflow)?;
150 let mut buf = vec![0; buf_size];
151 self.file
152 .read_exact(&mut buf)
153 .map_err(StrfileError::GetQuote)?;
154 let mut quote = String::from_utf8_lossy(&buf).to_string();
155
156 if self.metadata.is_encrypted() {
157 let view = unsafe { quote.as_mut_vec() };
158 for i in &mut *view {
159 match i {
160 b'a'..=b'm' | b'A'..=b'M' => *i += 13,
161 b'n'..=b'z' | b'N'..=b'Z' => *i -= 13,
162 _ => (),
163 }
164 }
165 }
166
167 Ok(quote)
168 }
169}