Skip to main content

lolcow_fortune/
lib.rs

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}