cornell_notes/
lib.rs

1//! # Cornell Notes Library
2//!
3//! A Rust library for reading, writing, and validating Cornell Notes in standardized JSON format.
4//!
5//! This library implements the Cornell Notes Data Schema RFC, providing type-safe structures
6//! and validation for the three-section note-taking system.
7//!
8//! ## Example
9//!
10//! ```no_run
11//! use cornell_notes::{CornellNote, Metadata};
12//! use std::fs;
13//!
14//! // Read a Cornell Note from file
15//! let json = fs::read_to_string("note.json").unwrap();
16//! let note = cornell_notes::from_json(&json).unwrap();
17//!
18//! // Validate the note
19//! note.validate().unwrap();
20//!
21//! // Write back to JSON
22//! let output = cornell_notes::to_json(&note).unwrap();
23//! ```
24
25use chrono::{DateTime, Utc};
26use serde::{Deserialize, Serialize};
27use std::collections::HashSet;
28use thiserror::Error;
29use uuid::Uuid;
30
31/// Errors that can occur when working with Cornell Notes
32#[derive(Error, Debug)]
33pub enum CornellNoteError {
34    #[error("JSON parsing error: {0}")]
35    JsonError(#[from] serde_json::Error),
36
37    #[error("Validation error: {0}")]
38    ValidationError(String),
39
40    #[error("IO error: {0}")]
41    IoError(#[from] std::io::Error),
42
43    #[error("Invalid version format: {0}")]
44    InvalidVersion(String),
45
46    #[error("Invalid UUID: {0}")]
47    InvalidUuid(String),
48
49    #[error("Invalid datetime: {0}")]
50    InvalidDatetime(String),
51}
52
53pub type Result<T> = std::result::Result<T, CornellNoteError>;
54
55/// Type of cue entry
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
57#[serde(rename_all = "lowercase")]
58pub enum CueType {
59    Text,
60    Question,
61    Keyword,
62    Custom,
63}
64
65/// Type of note entry
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
67#[serde(rename_all = "lowercase")]
68pub enum NoteType {
69    Text,
70    List,
71    Code,
72    Image,
73    Link,
74    Custom,
75}
76
77/// Content format for structured content
78#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
79#[serde(rename_all = "lowercase")]
80pub enum ContentFormat {
81    Markdown,
82    Html,
83    Plain,
84}
85
86/// Attachment metadata
87#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
88pub struct Attachment {
89    #[serde(rename = "type")]
90    pub attachment_type: String,
91    pub url: String,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub metadata: Option<serde_json::Value>,
94}
95
96/// Structured content with format and optional attachments
97#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
98pub struct StructuredContent {
99    pub format: ContentFormat,
100    pub data: String,
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub attachments: Option<Vec<Attachment>>,
103}
104
105/// Content can be either a simple string or structured object
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
107#[serde(untagged)]
108pub enum Content {
109    Simple(String),
110    Structured(StructuredContent),
111}
112
113/// Metadata for the Cornell Note document
114#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
115pub struct Metadata {
116    pub id: Uuid,
117    pub title: String,
118    pub created: DateTime<Utc>,
119    pub modified: DateTime<Utc>,
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub subject: Option<String>,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub topic: Option<String>,
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub author: Option<String>,
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub tags: Option<Vec<String>>,
128}
129
130/// A cue entry in the left column
131#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
132pub struct Cue {
133    pub id: Uuid,
134    pub content: Content,
135    #[serde(rename = "type")]
136    pub cue_type: CueType,
137    pub position: u32,
138    pub created: DateTime<Utc>,
139    pub modified: DateTime<Utc>,
140}
141
142/// A note entry in the right column
143#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
144pub struct Note {
145    pub id: Uuid,
146    pub content: Content,
147    #[serde(rename = "type")]
148    pub note_type: NoteType,
149    pub position: u32,
150    pub created: DateTime<Utc>,
151    pub modified: DateTime<Utc>,
152    #[serde(skip_serializing_if = "Option::is_none", rename = "cueLinks")]
153    pub cue_links: Option<Vec<Uuid>>,
154}
155
156/// A section containing cues and notes
157#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
158pub struct Section {
159    pub id: Uuid,
160    pub cues: Vec<Cue>,
161    pub notes: Vec<Note>,
162    pub position: u32,
163}
164
165/// Summary section at the bottom
166#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
167pub struct Summary {
168    pub content: Content,
169    pub created: DateTime<Utc>,
170    pub modified: DateTime<Utc>,
171}
172
173/// Complete Cornell Note document
174#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
175pub struct CornellNote {
176    pub version: String,
177    pub metadata: Metadata,
178    pub sections: Vec<Section>,
179    pub summary: Summary,
180}
181
182impl Metadata {
183    /// Validate metadata fields
184    pub fn validate(&self) -> Result<()> {
185        if self.title.is_empty() {
186            return Err(CornellNoteError::ValidationError(
187                "Title cannot be empty".to_string(),
188            ));
189        }
190
191        if self.modified < self.created {
192            return Err(CornellNoteError::ValidationError(
193                "Modified date cannot be before created date".to_string(),
194            ));
195        }
196
197        Ok(())
198    }
199}
200
201impl CornellNote {
202    /// Create a new Cornell Note with default values
203    pub fn new(title: String) -> Self {
204        let now = Utc::now();
205        CornellNote {
206            version: "1.0".to_string(),
207            metadata: Metadata {
208                id: Uuid::new_v4(),
209                title,
210                created: now,
211                modified: now,
212                subject: None,
213                topic: None,
214                author: None,
215                tags: None,
216            },
217            sections: vec![],
218            summary: Summary {
219                content: Content::Simple(String::new()),
220                created: now,
221                modified: now,
222            },
223        }
224    }
225
226    /// Validate the entire Cornell Note document
227    pub fn validate(&self) -> Result<()> {
228        // Validate version format
229        let version_regex = regex::Regex::new(r"^[0-9]+\.[0-9]+$").unwrap();
230        if !version_regex.is_match(&self.version) {
231            return Err(CornellNoteError::InvalidVersion(format!(
232                "Version '{}' does not match required format (e.g., '1.0')",
233                self.version
234            )));
235        }
236
237        // Validate metadata
238        self.metadata.validate()?;
239
240        // Validate sections
241        if self.sections.is_empty() {
242            return Err(CornellNoteError::ValidationError(
243                "Document must contain at least one section".to_string(),
244            ));
245        }
246
247        // Track all section IDs to check for duplicates
248        let mut section_ids = HashSet::new();
249        let mut section_positions = HashSet::new();
250
251        for section in &self.sections {
252            // Check for duplicate section IDs
253            if !section_ids.insert(section.id) {
254                return Err(CornellNoteError::ValidationError(format!(
255                    "Duplicate section ID: {}",
256                    section.id
257                )));
258            }
259
260            // Check for duplicate positions
261            if !section_positions.insert(section.position) {
262                return Err(CornellNoteError::ValidationError(format!(
263                    "Duplicate section position: {}",
264                    section.position
265                )));
266            }
267
268            section.validate()?;
269        }
270
271        // Validate summary
272        self.summary.validate()?;
273
274        Ok(())
275    }
276
277    /// Add a new section to the note
278    pub fn add_section(&mut self, section: Section) {
279        self.sections.push(section);
280        self.metadata.modified = Utc::now();
281    }
282
283    /// Update the summary
284    pub fn update_summary(&mut self, content: Content) {
285        self.summary.content = content;
286        self.summary.modified = Utc::now();
287        self.metadata.modified = Utc::now();
288    }
289}
290
291impl Section {
292    /// Create a new empty section
293    pub fn new(position: u32) -> Self {
294        Section {
295            id: Uuid::new_v4(),
296            cues: vec![],
297            notes: vec![],
298            position,
299        }
300    }
301
302    /// Validate a section
303    pub fn validate(&self) -> Result<()> {
304        // Collect all cue IDs for later validation of cue links
305        let mut cue_ids = HashSet::new();
306        let mut cue_positions = HashSet::new();
307
308        for cue in &self.cues {
309            if !cue_ids.insert(cue.id) {
310                return Err(CornellNoteError::ValidationError(format!(
311                    "Duplicate cue ID in section: {}",
312                    cue.id
313                )));
314            }
315
316            if !cue_positions.insert(cue.position) {
317                return Err(CornellNoteError::ValidationError(format!(
318                    "Duplicate cue position in section: {}",
319                    cue.position
320                )));
321            }
322
323            cue.validate()?;
324        }
325
326        let mut note_ids = HashSet::new();
327        let mut note_positions = HashSet::new();
328
329        for note in &self.notes {
330            if !note_ids.insert(note.id) {
331                return Err(CornellNoteError::ValidationError(format!(
332                    "Duplicate note ID in section: {}",
333                    note.id
334                )));
335            }
336
337            if !note_positions.insert(note.position) {
338                return Err(CornellNoteError::ValidationError(format!(
339                    "Duplicate note position in section: {}",
340                    note.position
341                )));
342            }
343
344            note.validate()?;
345
346            // Validate cue links reference actual cues
347            if let Some(ref links) = note.cue_links {
348                for link in links {
349                    if !cue_ids.contains(link) {
350                        return Err(CornellNoteError::ValidationError(format!(
351                            "Note {} references non-existent cue: {}",
352                            note.id, link
353                        )));
354                    }
355                }
356            }
357        }
358
359        Ok(())
360    }
361
362    /// Add a cue to this section
363    pub fn add_cue(&mut self, cue: Cue) {
364        self.cues.push(cue);
365    }
366
367    /// Add a note to this section
368    pub fn add_note(&mut self, note: Note) {
369        self.notes.push(note);
370    }
371}
372
373impl Cue {
374    /// Create a new cue
375    pub fn new(content: Content, cue_type: CueType, position: u32) -> Self {
376        let now = Utc::now();
377        Cue {
378            id: Uuid::new_v4(),
379            content,
380            cue_type,
381            position,
382            created: now,
383            modified: now,
384        }
385    }
386
387    /// Validate a cue
388    pub fn validate(&self) -> Result<()> {
389        if self.modified < self.created {
390            return Err(CornellNoteError::ValidationError(format!(
391                "Cue {} has modified date before created date",
392                self.id
393            )));
394        }
395        Ok(())
396    }
397}
398
399impl Note {
400    /// Create a new note
401    pub fn new(content: Content, note_type: NoteType, position: u32) -> Self {
402        let now = Utc::now();
403        Note {
404            id: Uuid::new_v4(),
405            content,
406            note_type,
407            position,
408            created: now,
409            modified: now,
410            cue_links: None,
411        }
412    }
413
414    /// Create a new note with cue links
415    pub fn new_with_links(
416        content: Content,
417        note_type: NoteType,
418        position: u32,
419        cue_links: Vec<Uuid>,
420    ) -> Self {
421        let now = Utc::now();
422        Note {
423            id: Uuid::new_v4(),
424            content,
425            note_type,
426            position,
427            created: now,
428            modified: now,
429            cue_links: Some(cue_links),
430        }
431    }
432
433    /// Validate a note
434    pub fn validate(&self) -> Result<()> {
435        if self.modified < self.created {
436            return Err(CornellNoteError::ValidationError(format!(
437                "Note {} has modified date before created date",
438                self.id
439            )));
440        }
441        Ok(())
442    }
443}
444
445impl Summary {
446    /// Create a new summary
447    pub fn new(content: Content) -> Self {
448        let now = Utc::now();
449        Summary {
450            content,
451            created: now,
452            modified: now,
453        }
454    }
455
456    /// Validate a summary
457    pub fn validate(&self) -> Result<()> {
458        if self.modified < self.created {
459            return Err(CornellNoteError::ValidationError(
460                "Summary has modified date before created date".to_string(),
461            ));
462        }
463        Ok(())
464    }
465}
466
467/// Parse a Cornell Note from JSON string
468pub fn from_json(json: &str) -> Result<CornellNote> {
469    let note: CornellNote = serde_json::from_str(json)?;
470    note.validate()?;
471    Ok(note)
472}
473
474/// Parse a Cornell Note from JSON string without validation
475pub fn from_json_unchecked(json: &str) -> Result<CornellNote> {
476    Ok(serde_json::from_str(json)?)
477}
478
479/// Serialize a Cornell Note to JSON string
480pub fn to_json(note: &CornellNote) -> Result<String> {
481    Ok(serde_json::to_string(note)?)
482}
483
484/// Serialize a Cornell Note to pretty-printed JSON string
485pub fn to_json_pretty(note: &CornellNote) -> Result<String> {
486    Ok(serde_json::to_string_pretty(note)?)
487}
488
489/// Read a Cornell Note from a file
490pub fn read_from_file(path: &str) -> Result<CornellNote> {
491    let contents = std::fs::read_to_string(path)?;
492    from_json(&contents)
493}
494
495/// Write a Cornell Note to a file
496pub fn write_to_file(note: &CornellNote, path: &str) -> Result<()> {
497    let json = to_json_pretty(note)?;
498    std::fs::write(path, json)?;
499    Ok(())
500}
501
502/// Helper function to extract content as a string
503fn content_to_string(content: &Content) -> String {
504    match content {
505        Content::Simple(s) => s.clone(),
506        Content::Structured(structured) => structured.data.clone(),
507    }
508}
509
510/// Export a Cornell Note to Markdown format
511pub fn to_markdown(note: &CornellNote) -> String {
512    let mut output = String::new();
513
514    // Title
515    output.push_str(&format!("# {}\n\n", note.metadata.title));
516
517    // Optional metadata
518    if let Some(ref subject) = note.metadata.subject {
519        output.push_str(&format!("**Subject:** {}\n\n", subject));
520    }
521    if let Some(ref topic) = note.metadata.topic {
522        output.push_str(&format!("**Topic:** {}\n\n", topic));
523    }
524    if let Some(ref author) = note.metadata.author {
525        output.push_str(&format!("**Author:** {}\n\n", author));
526    }
527    if let Some(ref tags) = note.metadata.tags {
528        if !tags.is_empty() {
529            output.push_str(&format!("**Tags:** {}\n\n", tags.join(", ")));
530        }
531    }
532
533    // Sections
534    let mut sorted_sections = note.sections.clone();
535    sorted_sections.sort_by_key(|s| s.position);
536
537    for section in sorted_sections {
538        output.push_str(&format!("## Section {}\n\n", section.position + 1));
539
540        // Cues
541        if !section.cues.is_empty() {
542            output.push_str("### Cues\n\n");
543            let mut sorted_cues = section.cues.clone();
544            sorted_cues.sort_by_key(|c| c.position);
545
546            for cue in sorted_cues {
547                let content = content_to_string(&cue.content);
548                output.push_str(&format!("- {}\n", content));
549            }
550            output.push('\n');
551        }
552
553        // Notes
554        if !section.notes.is_empty() {
555            output.push_str("### Notes\n\n");
556            let mut sorted_notes = section.notes.clone();
557            sorted_notes.sort_by_key(|n| n.position);
558
559            for note_entry in sorted_notes {
560                let content = content_to_string(&note_entry.content);
561                output.push_str(&content);
562                output.push_str("\n\n");
563            }
564        }
565    }
566
567    // Summary
568    output.push_str("## Summary\n\n");
569    let summary_content = content_to_string(&note.summary.content);
570    output.push_str(&summary_content);
571    output.push('\n');
572
573    output
574}
575
576/// Write a Cornell Note to a Markdown file
577pub fn write_to_markdown_file(note: &CornellNote, path: &str) -> Result<()> {
578    let markdown = to_markdown(note);
579    std::fs::write(path, markdown)?;
580    Ok(())
581}
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586
587    #[test]
588    fn test_create_new_note() {
589        let note = CornellNote::new("Test Note".to_string());
590        assert_eq!(note.version, "1.0");
591        assert_eq!(note.metadata.title, "Test Note");
592        assert!(note.sections.is_empty());
593    }
594
595    #[test]
596    fn test_validation_requires_sections() {
597        let note = CornellNote::new("Test".to_string());
598        assert!(note.validate().is_err());
599    }
600
601    #[test]
602    fn test_version_validation() {
603        let mut note = CornellNote::new("Test".to_string());
604        note.version = "invalid".to_string();
605        let result = note.validate();
606        assert!(result.is_err());
607        match result {
608            Err(CornellNoteError::InvalidVersion(_)) => (),
609            _ => panic!("Expected InvalidVersion error"),
610        }
611    }
612
613    #[test]
614    fn test_empty_title_validation() {
615        let mut note = CornellNote::new("".to_string());
616        let mut section = Section::new(0);
617        section.add_cue(Cue::new(
618            Content::Simple("Test cue".to_string()),
619            CueType::Text,
620            0,
621        ));
622        note.add_section(section);
623        assert!(note.validate().is_err());
624    }
625
626    #[test]
627    fn test_valid_note_with_section() {
628        let mut note = CornellNote::new("Valid Note".to_string());
629        let mut section = Section::new(0);
630
631        let cue = Cue::new(
632            Content::Simple("What is Rust?".to_string()),
633            CueType::Question,
634            0,
635        );
636        section.add_cue(cue);
637
638        let note_entry = Note::new(
639            Content::Simple("A systems programming language".to_string()),
640            NoteType::Text,
641            0,
642        );
643        section.add_note(note_entry);
644
645        note.add_section(section);
646        assert!(note.validate().is_ok());
647    }
648
649    #[test]
650    fn test_cue_links_validation() {
651        let mut note = CornellNote::new("Test".to_string());
652        let mut section = Section::new(0);
653
654        let cue = Cue::new(Content::Simple("Cue".to_string()), CueType::Text, 0);
655        let cue_id = cue.id;
656        section.add_cue(cue);
657
658        // Valid cue link
659        let note_entry = Note::new_with_links(
660            Content::Simple("Note".to_string()),
661            NoteType::Text,
662            0,
663            vec![cue_id],
664        );
665        section.add_note(note_entry);
666
667        note.add_section(section);
668        assert!(note.validate().is_ok());
669    }
670
671    #[test]
672    fn test_invalid_cue_links() {
673        let mut note = CornellNote::new("Test".to_string());
674        let mut section = Section::new(0);
675
676        // Add note with link to non-existent cue
677        let note_entry = Note::new_with_links(
678            Content::Simple("Note".to_string()),
679            NoteType::Text,
680            0,
681            vec![Uuid::new_v4()],
682        );
683        section.add_note(note_entry);
684
685        note.add_section(section);
686        assert!(note.validate().is_err());
687    }
688
689    #[test]
690    fn test_structured_content() {
691        let structured = StructuredContent {
692            format: ContentFormat::Markdown,
693            data: "# Header\n\nContent".to_string(),
694            attachments: None,
695        };
696
697        let content = Content::Structured(structured);
698        let cue = Cue::new(content, CueType::Text, 0);
699        assert!(cue.validate().is_ok());
700    }
701
702    #[test]
703    fn test_json_roundtrip() {
704        let mut note = CornellNote::new("Roundtrip Test".to_string());
705        let mut section = Section::new(0);
706
707        section.add_cue(Cue::new(
708            Content::Simple("Question?".to_string()),
709            CueType::Question,
710            0,
711        ));
712        section.add_note(Note::new(
713            Content::Simple("Answer".to_string()),
714            NoteType::Text,
715            0,
716        ));
717
718        note.add_section(section);
719
720        let json = to_json_pretty(&note).unwrap();
721        let parsed = from_json(&json).unwrap();
722
723        assert_eq!(note, parsed);
724    }
725
726    #[test]
727    fn test_duplicate_section_ids() {
728        let mut note = CornellNote::new("Test".to_string());
729        let section1 = Section::new(0);
730        let mut section2 = Section::new(1);
731        section2.id = section1.id; // Duplicate ID
732
733        note.add_section(section1);
734        note.add_section(section2);
735
736        assert!(note.validate().is_err());
737    }
738
739    #[test]
740    fn test_duplicate_cue_positions() {
741        let mut note = CornellNote::new("Test".to_string());
742        let mut section = Section::new(0);
743
744        section.add_cue(Cue::new(
745            Content::Simple("Cue 1".to_string()),
746            CueType::Text,
747            0,
748        ));
749        section.add_cue(Cue::new(
750            Content::Simple("Cue 2".to_string()),
751            CueType::Text,
752            0, // Duplicate position
753        ));
754
755        note.add_section(section);
756        assert!(note.validate().is_err());
757    }
758
759    #[test]
760    fn test_markdown_export() {
761        let mut note = CornellNote::new("Introduction to Rust".to_string());
762        note.metadata.subject = Some("Computer Science".to_string());
763        note.metadata.topic = Some("Programming Languages".to_string());
764        note.metadata.author = Some("Student".to_string());
765        note.metadata.tags = Some(vec!["rust".to_string(), "programming".to_string()]);
766
767        let mut section = Section::new(0);
768
769        section.add_cue(Cue::new(
770            Content::Simple("What is Rust?".to_string()),
771            CueType::Question,
772            0,
773        ));
774        section.add_cue(Cue::new(
775            Content::Simple("Memory Safety".to_string()),
776            CueType::Keyword,
777            1,
778        ));
779
780        section.add_note(Note::new(
781            Content::Simple("A systems programming language".to_string()),
782            NoteType::Text,
783            0,
784        ));
785        section.add_note(Note::new(
786            Content::Structured(StructuredContent {
787                format: ContentFormat::Markdown,
788                data: "Rust provides memory safety without GC".to_string(),
789                attachments: None,
790            }),
791            NoteType::Text,
792            1,
793        ));
794
795        note.add_section(section);
796        note.update_summary(Content::Simple(
797            "Rust is a safe systems language".to_string(),
798        ));
799
800        let markdown = to_markdown(&note);
801
802        assert!(markdown.contains("# Introduction to Rust"));
803        assert!(markdown.contains("**Subject:** Computer Science"));
804        assert!(markdown.contains("**Topic:** Programming Languages"));
805        assert!(markdown.contains("**Author:** Student"));
806        assert!(markdown.contains("**Tags:** rust, programming"));
807        assert!(markdown.contains("## Section 1"));
808        assert!(markdown.contains("### Cues"));
809        assert!(markdown.contains("- What is Rust?"));
810        assert!(markdown.contains("- Memory Safety"));
811        assert!(markdown.contains("### Notes"));
812        assert!(markdown.contains("A systems programming language"));
813        assert!(markdown.contains("Rust provides memory safety without GC"));
814        assert!(markdown.contains("## Summary"));
815        assert!(markdown.contains("Rust is a safe systems language"));
816    }
817
818    #[test]
819    fn test_markdown_file_write() {
820        let mut note = CornellNote::new("Test Note".to_string());
821        let mut section = Section::new(0);
822
823        section.add_cue(Cue::new(
824            Content::Simple("Cue".to_string()),
825            CueType::Text,
826            0,
827        ));
828        section.add_note(Note::new(
829            Content::Simple("Note".to_string()),
830            NoteType::Text,
831            0,
832        ));
833
834        note.add_section(section);
835        note.update_summary(Content::Simple("Summary".to_string()));
836
837        // Use cross-platform temp directory
838        let temp_dir = std::env::temp_dir();
839        let temp_file = temp_dir.join("test_note.md");
840        write_to_markdown_file(&note, temp_file.to_str().unwrap()).unwrap();
841
842        let contents = std::fs::read_to_string(&temp_file).unwrap();
843        assert!(contents.contains("# Test Note"));
844        assert!(contents.contains("## Section 1"));
845        assert!(contents.contains("### Cues"));
846        assert!(contents.contains("- Cue"));
847        assert!(contents.contains("### Notes"));
848        assert!(contents.contains("Note"));
849        assert!(contents.contains("## Summary"));
850        assert!(contents.contains("Summary"));
851
852        std::fs::remove_file(temp_file).ok();
853    }
854}