Skip to main content

ds_decomp/config/
delinks.rs

1use std::{
2    backtrace::Backtrace,
3    fmt::Display,
4    io::{self, BufWriter, Write},
5    iter::Peekable,
6    path::Path,
7};
8
9use snafu::Snafu;
10
11use super::{
12    ParseContext,
13    module::ModuleKind,
14    section::{Section, SectionInheritParseError, SectionParseError, Sections, SectionsError},
15};
16use crate::{
17    config::{CommentedLine, CommentedLineIterator, Comments},
18    util::io::{FileError, create_file},
19};
20
21pub struct Delinks {
22    pub sections: Sections,
23    pub global_categories: Categories,
24    module_kind: ModuleKind,
25    pub files: Vec<DelinkFile>,
26}
27
28#[derive(Debug, Snafu)]
29pub enum DelinksParseError {
30    #[snafu(transparent)]
31    File { source: FileError },
32    #[snafu(transparent)]
33    Io { source: io::Error },
34    #[snafu(transparent)]
35    SectionParse { source: SectionParseError },
36    #[snafu(display("{context}: {error}"))]
37    Sections { context: ParseContext, error: Box<SectionsError> },
38    #[snafu(transparent)]
39    DelinkFileParse { source: DelinkFileParseError },
40}
41
42#[derive(Debug, Snafu)]
43pub enum DelinksWriteError {
44    #[snafu(transparent)]
45    File { source: FileError },
46    #[snafu(transparent)]
47    Io { source: io::Error },
48}
49
50impl Delinks {
51    pub fn new(sections: Sections, files: Vec<DelinkFile>, module_kind: ModuleKind) -> Self {
52        Self { sections, global_categories: Categories::new(), files, module_kind }
53    }
54
55    pub fn from_file<P: AsRef<Path>>(
56        path: P,
57        module_kind: ModuleKind,
58    ) -> Result<Self, DelinksParseError> {
59        let path = path.as_ref();
60
61        let mut context = ParseContext { file_path: path.to_str().unwrap().to_string(), row: 0 };
62        let mut lines = CommentedLine::read(path)?.peekable();
63
64        let mut sections: Sections = Sections::new();
65        let mut files = vec![];
66        let mut global_categories = Categories::new();
67
68        while let Some(line) = lines.next() {
69            let line = line?;
70            context.row = line.row;
71            if line.text.trim().is_empty() {
72                continue;
73            } else if let Some(delink_file) =
74                Self::try_parse_delink_file(&line, &mut lines, &mut context, &sections)?
75            {
76                files.push(delink_file);
77                break;
78            } else if let Some(new_categories) = Categories::try_parse(&line) {
79                global_categories.extend(new_categories);
80            } else {
81                let section = Section::parse(&line, &context)?;
82                sections
83                    .add(section)
84                    .map_err(|error| SectionsSnafu { context: context.clone(), error }.build())?;
85            }
86        }
87
88        while let Some(line) = lines.next() {
89            let line = line?;
90            context.row = line.row;
91
92            if line.text.trim().is_empty() {
93                continue;
94            } else if let Some(delink_file) =
95                Self::try_parse_delink_file(&line, &mut lines, &mut context, &sections)?
96            {
97                files.push(delink_file);
98            }
99        }
100
101        Ok(Self { sections, global_categories, files, module_kind })
102    }
103
104    fn try_parse_delink_file(
105        line: &CommentedLine,
106        lines: &mut Peekable<CommentedLineIterator>,
107        context: &mut ParseContext,
108        sections: &Sections,
109    ) -> Result<Option<DelinkFile>, DelinkFileParseError> {
110        if line.text.chars().next().is_some_and(|c| !c.is_whitespace()) {
111            let delink_file = DelinkFile::parse(line, lines, context, sections)?;
112            Ok(Some(delink_file))
113        } else {
114            Ok(None)
115        }
116    }
117
118    pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), DelinksWriteError> {
119        let path = path.as_ref();
120
121        let file = create_file(path)?;
122        let mut writer = BufWriter::new(file);
123        write!(writer, "{self}")?;
124
125        Ok(())
126    }
127
128    pub fn module_kind(&self) -> ModuleKind {
129        self.module_kind
130    }
131}
132
133impl Display for Delinks {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        if !self.global_categories.categories.is_empty() {
136            writeln!(f, "{}", self.global_categories)?;
137        }
138        for section in self.sections.sorted_by_address() {
139            writeln!(f, "{section}")?;
140        }
141        for file in &self.files {
142            if file.gap {
143                continue;
144            }
145            writeln!(f)?;
146            write!(f, "{file}")?;
147        }
148        Ok(())
149    }
150}
151
152#[derive(Clone)]
153pub struct DelinkFile {
154    pub name: String,
155    pub sections: Sections,
156    pub migrated_sections: Sections,
157    pub complete: bool,
158    pub categories: Categories,
159    gap: bool,
160    migrated: bool,
161    pub comments: Comments,
162}
163
164#[derive(Debug, Snafu)]
165pub enum DelinkFileParseError {
166    #[snafu(display("{context}: expected file path to end with ':':\n{backtrace}"))]
167    MissingColon { context: ParseContext, backtrace: Backtrace },
168    #[snafu(transparent)]
169    Io { source: io::Error },
170    #[snafu(transparent)]
171    SectionInheritParse { source: SectionInheritParseError },
172    #[snafu(transparent)]
173    Sections { source: SectionsError },
174}
175
176pub struct DelinkFileOptions {
177    pub name: String,
178    pub sections: Sections,
179    pub complete: bool,
180    pub categories: Categories,
181    pub gap: bool,
182    pub migrated: bool,
183    pub comments: Comments,
184}
185
186impl DelinkFile {
187    pub fn new(options: DelinkFileOptions) -> Self {
188        let DelinkFileOptions { name, sections, complete, categories, gap, migrated, mut comments } =
189            options;
190        comments.remove_leading_blank_lines();
191        Self {
192            name,
193            sections,
194            migrated_sections: Sections::new(),
195            complete,
196            categories,
197            gap,
198            migrated,
199            comments,
200        }
201    }
202
203    pub fn parse(
204        first_line: &CommentedLine,
205        lines: &mut Peekable<CommentedLineIterator>,
206        context: &mut ParseContext,
207        inherit_sections: &Sections,
208    ) -> Result<Self, DelinkFileParseError> {
209        let name = first_line
210            .text
211            .trim()
212            .strip_suffix(':')
213            .ok_or_else(|| MissingColonSnafu { context: context.clone() }.build())?
214            .to_string();
215
216        let mut complete = false;
217        let mut sections = Sections::new();
218        let mut categories = Categories::new();
219
220        loop {
221            if let Some(Ok(next)) = lines.peek()
222                && next.text.chars().next().is_some_and(|c| !c.is_whitespace())
223            {
224                break;
225            }
226
227            let Some(line) = lines.next() else {
228                break;
229            };
230            let line = line?;
231            context.row = line.row;
232            let text = line.text.trim();
233            if text.is_empty() {
234                break;
235            } else if text == "complete" {
236                complete = true;
237            } else if let Some(new_categories) = Categories::try_parse(&line) {
238                categories.extend(new_categories);
239            } else {
240                let section = Section::parse_inherit(&line, context, inherit_sections)?;
241                sections.add(section)?;
242            }
243        }
244
245        Ok(DelinkFile::new(DelinkFileOptions {
246            name,
247            sections,
248            complete,
249            categories,
250            gap: false,
251            migrated: false,
252            comments: first_line.comments.clone(),
253        }))
254    }
255
256    pub fn split_file_ext(&self) -> (&str, &str) {
257        self.name.rsplit_once('.').unwrap_or((&self.name, ""))
258    }
259
260    pub fn gap(&self) -> bool {
261        self.gap
262    }
263
264    pub fn migrated(&self) -> bool {
265        self.migrated
266    }
267
268    pub fn migrate_section_by_name(&mut self, name: &str) -> Result<(), SectionsError> {
269        let Some(section) = self.sections.remove(name) else {
270            return Ok(());
271        };
272        self.migrated_sections.add(section)?;
273        Ok(())
274    }
275}
276
277impl Display for DelinkFile {
278    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279        write!(f, "{}", self.comments.display_pre_comments())?;
280        write!(f, "{}:", self.name)?;
281        writeln!(f, "{}", self.comments.display_post_comment())?;
282
283        if !self.categories.categories.is_empty() {
284            writeln!(f, "{}", self.categories)?;
285        }
286        if self.complete {
287            writeln!(f, "    complete")?;
288        }
289        for section in self.sections.sorted_by_address() {
290            section.write_inherit(f)?;
291            writeln!(f)?;
292        }
293        Ok(())
294    }
295}
296
297#[derive(Clone)]
298pub struct Categories {
299    pub categories: Vec<String>,
300    comments: Comments,
301}
302
303impl Categories {
304    pub fn new() -> Self {
305        Self { categories: Vec::new(), comments: Comments::new() }
306    }
307
308    pub fn try_parse(line: &CommentedLine) -> Option<Self> {
309        let list = line.text.trim().strip_prefix("categories:")?;
310        let categories =
311            list.trim().split(',').map(|category| category.trim().to_string()).collect();
312        Some(Self { categories, comments: line.comments.clone() })
313    }
314
315    pub fn extend(&mut self, other: Categories) {
316        self.categories.extend(other.categories);
317        self.categories.sort_unstable();
318        self.categories.dedup();
319    }
320}
321
322impl Default for Categories {
323    fn default() -> Self {
324        Self::new()
325    }
326}
327
328impl Display for Categories {
329    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330        write!(f, "{}", self.comments.display_pre_comments())?;
331        let mut iter = self.categories.iter();
332        if let Some(category) = iter.next() {
333            write!(f, "    categories: {category}")?;
334        }
335        for category in iter {
336            write!(f, ", {category}")?;
337        }
338        write!(f, "{}", self.comments.display_post_comment())?;
339        Ok(())
340    }
341}