1use std::{
2 fmt::Debug,
3 path::{Path, PathBuf},
4};
5
6use base64::{prelude::BASE64_STANDARD, Engine};
7use core::str;
8use simple_crypt::{decrypt, encrypt};
9use thiserror::Error;
10
11use crate::{
12 data::{Answers, Puzzle, Session},
13 Day, Part, Year,
14};
15
16#[derive(Debug, Error)]
18pub enum CacheError {
19 #[error("passphrase expected but not provided (check your config)")]
20 PassphraseRequired,
21 #[error("Cached input file is not encrypted but encryption passphrase was provided")]
22 PassphraseNotNeeded,
24 #[error("base 64 decoding failed: {}", .0)]
25 DecodeBase64(#[from] base64::DecodeError),
26 #[error("decryption failed: {}", .0)]
27 Decryption(#[source] anyhow::Error),
28 #[error("encryption failed: {}", .0)]
29 Encryption(#[source] anyhow::Error),
30 #[error("decoding utf8 failed: {}", .0)]
31 DecodeUtf8(#[from] std::string::FromUtf8Error),
32 #[error("serializing or deserializing failed: {}", .0)]
33 JsonSerde(#[from] serde_json::Error),
34 #[error("a file i/o error occured while reading/writing the cache: {}", .0)]
35 Io(#[from] std::io::Error),
36 #[error("an error occurred while parsing a cached answer dataset: {}", .0)]
37 AnswerParsing(#[from] crate::data::AnswerDeserializationError),
38}
39
40pub trait PuzzleCache: Debug {
46 fn load_input(&self, day: Day, year: Year) -> Result<Option<String>, CacheError>;
49
50 fn load_answers(&self, part: Part, day: Day, year: Year)
52 -> Result<Option<Answers>, CacheError>;
53
54 fn save(&self, puzzle: Puzzle) -> Result<(), CacheError> {
56 self.save_input(&puzzle.input, puzzle.day, puzzle.year)?;
57 self.save_answers(&puzzle.part_one_answers, Part::One, puzzle.day, puzzle.year)?;
58 self.save_answers(&puzzle.part_two_answers, Part::Two, puzzle.day, puzzle.year)?;
59
60 Ok(())
61 }
62
63 fn save_input(&self, input: &str, day: Day, year: Year) -> Result<(), CacheError>;
67
68 fn save_answers(
71 &self,
72 answers: &Answers,
73 part: Part,
74 day: Day,
75 year: Year,
76 ) -> Result<(), CacheError>;
77}
78
79pub trait SessionCache: Debug {
81 fn load(&self, session_id: &str) -> Result<Session, CacheError> {
83 if let Some(session) = self.try_load(session_id)? {
84 Ok(session)
85 } else {
86 tracing::debug!("session {session_id} was not cached; returning new Session object");
87 Ok(Session::new(session_id))
88 }
89 }
90
91 fn try_load(&self, session_id: &str) -> Result<Option<Session>, CacheError>;
94
95 fn save(&self, session: &Session) -> Result<(), CacheError>;
97}
98
99#[derive(Debug)]
116pub struct PuzzleFsCache {
117 cache_dir: PathBuf,
118 passphrase: Option<String>,
119}
120
121impl PuzzleFsCache {
122 const INPUT_FILE_NAME: &'static str = "input.txt";
123 const ENCRYPTED_INPUT_FILE_NAME: &'static str = "input.encrypted.txt";
124 const PART_ONE_ANSWERS_FILE_NAME: &'static str = "part-1-answers.txt";
125 const PART_TWO_ANSWERS_FILE_NAME: &'static str = "part-2-answers.txt";
126
127 pub fn new<P: Into<PathBuf>, S: Into<String>>(cache_dir: P, passphrase: Option<S>) -> Self {
130 Self {
131 cache_dir: cache_dir.into(),
132 passphrase: passphrase.map(|x| x.into()),
133 }
134 }
135
136 pub fn dir_for_puzzle(cache_dir: &Path, day: Day, year: Year) -> PathBuf {
138 cache_dir.join(format!("y{}", year)).join(day.to_string())
139 }
140
141 pub fn input_file_path(cache_dir: &Path, day: Day, year: Year, encrypted: bool) -> PathBuf {
143 Self::dir_for_puzzle(cache_dir, day, year).join(if encrypted {
144 Self::ENCRYPTED_INPUT_FILE_NAME
145 } else {
146 Self::INPUT_FILE_NAME
147 })
148 }
149
150 pub fn answers_file_path(cache_dir: &Path, part: Part, day: Day, year: Year) -> PathBuf {
152 Self::dir_for_puzzle(cache_dir, day, year).join(match part {
153 Part::One => Self::PART_ONE_ANSWERS_FILE_NAME,
154 Part::Two => Self::PART_TWO_ANSWERS_FILE_NAME,
155 })
156 }
157}
158
159impl PuzzleCache for PuzzleFsCache {
160 fn load_input(&self, day: Day, year: Year) -> Result<Option<String>, CacheError> {
161 let using_encryption = self.passphrase.is_some();
164 let input_path = Self::input_file_path(&self.cache_dir, day, year, using_encryption);
165 let input_path_exists = std::fs::exists(&input_path).unwrap_or(false);
166
167 let alt_input_path = Self::input_file_path(&self.cache_dir, day, year, !using_encryption);
168 let alt_input_path_exists = std::fs::exists(alt_input_path).unwrap_or(false);
169
170 match (using_encryption, input_path_exists, alt_input_path_exists) {
171 (true, true, true) => {
172 tracing::warn!(
173 "mixed input (encrypted and unencrypted) for year {year} day {day} found in cache"
174 );
175 }
176 (true, false, true) => return Err(CacheError::PassphraseNotNeeded),
177 (false, true, true) => {
178 tracing::warn!(
179 "mixed input (encrypted and unencrypted) input for year {year} day {day} found in cache"
180 );
181 }
182 (false, false, true) => return Err(CacheError::PassphraseRequired),
183 (_, false, _) => return Ok(None),
184 _ => {}
185 }
186
187 tracing::debug!("loading input for day {day} year {year} from {input_path:?}");
189
190 match std::fs::read_to_string(input_path) {
191 Ok(input_text) => {
192 if let Some(passphrase) = &self.passphrase {
194 let encrypted_bytes = BASE64_STANDARD
196 .decode(input_text.as_bytes())
197 .map_err(CacheError::DecodeBase64)?;
198 let input_bytes = decrypt(&encrypted_bytes, passphrase.as_bytes())
199 .map_err(CacheError::Decryption)?;
200 let decrypted_input_text =
201 String::from_utf8(input_bytes).map_err(CacheError::DecodeUtf8)?;
202
203 tracing::debug!("succesfully decrypted input for puzzle day {day} year {year}");
204
205 Ok(Some(decrypted_input_text))
206 } else {
207 Ok(Some(input_text))
209 }
210 }
211 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
212 Err(e) => Err(CacheError::Io(e)),
213 }
214 }
215
216 fn load_answers(
217 &self,
218 part: Part,
219 day: Day,
220 year: Year,
221 ) -> Result<Option<Answers>, CacheError> {
222 match std::fs::read_to_string(Self::answers_file_path(&self.cache_dir, part, day, year)) {
223 Ok(answers_data) => Ok(Some(Answers::deserialize_from_str(&answers_data)?)),
224 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
225 Err(e) => Err(CacheError::Io(e)),
226 }
227 }
228
229 fn save(&self, puzzle: Puzzle) -> Result<(), CacheError> {
230 let puzzle_dir = Self::dir_for_puzzle(&self.cache_dir, puzzle.day, puzzle.year);
232 std::fs::create_dir_all(puzzle_dir)?;
233
234 self.save_input(&puzzle.input, puzzle.day, puzzle.year)?;
235 self.save_answers(&puzzle.part_one_answers, Part::One, puzzle.day, puzzle.year)?;
236 self.save_answers(&puzzle.part_two_answers, Part::Two, puzzle.day, puzzle.year)?;
237
238 Ok(())
239 }
240
241 fn save_input(&self, input: &str, day: Day, year: Year) -> Result<(), CacheError> {
242 let input_path =
244 Self::input_file_path(&self.cache_dir, day, year, self.passphrase.is_some());
245
246 let mut puzzle_dir = input_path.clone();
248 puzzle_dir.pop();
249
250 std::fs::create_dir_all(puzzle_dir)?;
251
252 if let Some(passphrase) = &self.passphrase {
254 let encrypted_data =
256 encrypt(input.as_bytes(), passphrase.as_bytes()).map_err(CacheError::Encryption)?;
257 let b64_encrypted_text = BASE64_STANDARD.encode(encrypted_data);
258
259 tracing::debug!("saving encrypted input for day {day} year {year} to {input_path:?}");
260 Ok(std::fs::write(input_path, b64_encrypted_text)?)
261 } else {
262 tracing::debug!("saving unencrypted input for day {day} year {year} to {input_path:?}");
264 Ok(std::fs::write(input_path, input)?)
265 }
266 }
267
268 fn save_answers(
269 &self,
270 answers: &Answers,
271 part: Part,
272 day: Day,
273 year: Year,
274 ) -> Result<(), CacheError> {
275 let answers_path = Self::answers_file_path(&self.cache_dir, part, day, year);
276
277 let mut puzzle_dir = answers_path.clone();
279 puzzle_dir.pop();
280
281 std::fs::create_dir_all(puzzle_dir)?;
282
283 tracing::debug!("saving answer for part {part} day {day} year {year} to {answers_path:?}");
284 Ok(std::fs::write(answers_path, answers.serialize_to_string())?)
285 }
286}
287
288#[derive(Debug)]
289pub struct SessionFsCache {
290 cache_dir: PathBuf,
291}
292
293impl SessionFsCache {
294 pub fn new<P: Into<PathBuf>>(cache_dir: P) -> Self {
295 Self {
296 cache_dir: cache_dir.into(),
297 }
298 }
299
300 pub fn session_data_filepath(&self, session_id: &str) -> PathBuf {
303 self.cache_dir.join(format!("{}.json", session_id))
304 }
305}
306
307impl SessionCache for SessionFsCache {
308 fn try_load(&self, session_id: &str) -> Result<Option<Session>, CacheError> {
309 let session_filepath = self.session_data_filepath(session_id);
310
311 if session_filepath.is_file() {
312 tracing::debug!("cached session data for {session_id} is at `{session_filepath:?}`");
313
314 let json_text = std::fs::read_to_string(session_filepath)?;
315 let session: Session = serde_json::from_str(&json_text)?;
316
317 Ok(Some(session))
318 } else {
319 tracing::debug!("session file `{session_filepath:?}` for {session_id} does not exist");
320 Ok(None)
321 }
322 }
323
324 fn save(&self, session: &Session) -> Result<(), CacheError> {
325 let session_filepath = self.session_data_filepath(&session.session_id);
326
327 let mut session_dir = session_filepath.clone();
329 session_dir.pop();
330
331 std::fs::create_dir_all(session_dir)?;
332
333 let json_text = serde_json::to_string(&session)?;
335 tracing::debug!("saving session data to `{session_filepath:?}`");
336
337 std::fs::write(session_filepath, json_text)?;
338 Ok(())
339 }
340}