ds_decomp/config/
delinks.rs

1use std::{
2    backtrace::Backtrace,
3    fmt::Display,
4    fs::File,
5    io::{self, BufRead, BufReader, BufWriter, Lines, Write},
6    path::Path,
7};
8
9use snafu::Snafu;
10
11use crate::util::io::{FileError, create_file, open_file};
12
13use super::{
14    ParseContext,
15    module::ModuleKind,
16    section::{Section, SectionInheritParseError, SectionParseError, Sections, SectionsError},
17};
18
19pub struct Delinks {
20    pub sections: Sections,
21    pub files: Vec<DelinkFile>,
22    module_kind: ModuleKind,
23}
24
25#[derive(Debug, Snafu)]
26pub enum DelinksParseError {
27    #[snafu(transparent)]
28    File { source: FileError },
29    #[snafu(transparent)]
30    Io { source: io::Error },
31    #[snafu(transparent)]
32    SectionParse { source: SectionParseError },
33    #[snafu(display("{context}: {error}"))]
34    Sections { context: ParseContext, error: Box<SectionsError> },
35    #[snafu(transparent)]
36    DelinkFileParse { source: DelinkFileParseError },
37}
38
39#[derive(Debug, Snafu)]
40pub enum DelinksWriteError {
41    #[snafu(transparent)]
42    File { source: FileError },
43    #[snafu(transparent)]
44    Io { source: io::Error },
45}
46
47impl Delinks {
48    pub fn new(sections: Sections, files: Vec<DelinkFile>, module_kind: ModuleKind) -> Self {
49        Self { sections, files, module_kind }
50    }
51
52    pub fn from_file<P: AsRef<Path>>(path: P, module_kind: ModuleKind) -> Result<Self, DelinksParseError> {
53        let path = path.as_ref();
54        let mut context = ParseContext { file_path: path.to_str().unwrap().to_string(), row: 0 };
55
56        let file = open_file(path)?;
57        let reader = BufReader::new(file);
58
59        let mut sections: Sections = Sections::new();
60        let mut files = vec![];
61
62        let mut lines = reader.lines();
63        while let Some(line) = lines.next() {
64            context.row += 1;
65
66            let line = line?;
67            let comment_start = line.find("//").unwrap_or(line.len());
68            let line = &line[..comment_start];
69
70            if Self::try_parse_delink_file(line, &mut lines, &mut context, &mut files, &sections)? {
71                break;
72            }
73            let Some(section) = Section::parse(line, &context)? else {
74                continue;
75            };
76            sections.add(section).map_err(|error| SectionsSnafu { context: context.clone(), error }.build())?;
77        }
78
79        while let Some(line) = lines.next() {
80            context.row += 1;
81
82            let line = line?;
83            let comment_start = line.find("//").unwrap_or(line.len());
84            let line = &line[..comment_start];
85
86            Self::try_parse_delink_file(line, &mut lines, &mut context, &mut files, &sections)?;
87        }
88
89        Ok(Self { sections, files, module_kind })
90    }
91
92    fn try_parse_delink_file(
93        line: &str,
94        lines: &mut Lines<BufReader<File>>,
95        context: &mut ParseContext,
96        files: &mut Vec<DelinkFile>,
97        sections: &Sections,
98    ) -> Result<bool, DelinkFileParseError> {
99        if line.chars().next().is_some_and(|c| !c.is_whitespace()) {
100            let delink_file = DelinkFile::parse(line, lines, context, sections)?;
101            files.push(delink_file);
102            Ok(true)
103        } else {
104            Ok(false)
105        }
106    }
107
108    pub fn to_file<P: AsRef<Path>>(path: P, sections: &Sections) -> Result<(), DelinksWriteError> {
109        let path = path.as_ref();
110
111        let file = create_file(path)?;
112        let mut writer = BufWriter::new(file);
113
114        write!(writer, "{}", DisplayDelinks { sections, files: &[] })?;
115
116        Ok(())
117    }
118
119    pub fn display(&self) -> DisplayDelinks {
120        DisplayDelinks { sections: &self.sections, files: &self.files }
121    }
122
123    pub fn module_kind(&self) -> ModuleKind {
124        self.module_kind
125    }
126}
127pub struct DisplayDelinks<'a> {
128    sections: &'a Sections,
129    files: &'a [DelinkFile],
130}
131
132impl Display for DisplayDelinks<'_> {
133    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134        for section in self.sections.sorted_by_address() {
135            writeln!(f, "    {section}")?;
136        }
137        writeln!(f)?;
138        for file in self.files {
139            writeln!(f, "{file}")?;
140        }
141        Ok(())
142    }
143}
144
145pub struct DelinkFile {
146    pub name: String,
147    pub sections: Sections,
148    pub complete: bool,
149    pub gap: bool,
150}
151
152#[derive(Debug, Snafu)]
153pub enum DelinkFileParseError {
154    #[snafu(display("{context}: expected file path to end with ':':\n{backtrace}"))]
155    MissingColon { context: ParseContext, backtrace: Backtrace },
156    #[snafu(transparent)]
157    Io { source: io::Error },
158    #[snafu(transparent)]
159    SectionInheritParse { source: SectionInheritParseError },
160    #[snafu(transparent)]
161    Sections { source: SectionsError },
162}
163
164impl DelinkFile {
165    pub fn new(name: String, sections: Sections, complete: bool) -> Self {
166        Self { name, sections, complete, gap: false }
167    }
168
169    pub fn parse(
170        first_line: &str,
171        lines: &mut Lines<BufReader<File>>,
172        context: &mut ParseContext,
173        inherit_sections: &Sections,
174    ) -> Result<Self, DelinkFileParseError> {
175        let name = first_line
176            .trim()
177            .strip_suffix(':')
178            .ok_or_else(|| MissingColonSnafu { context: context.clone() }.build())?
179            .to_string();
180
181        let mut complete = false;
182        let mut sections = Sections::new();
183        for line in lines.by_ref() {
184            context.row += 1;
185            let line = line?;
186            let line = line.trim();
187            if line.is_empty() {
188                break;
189            }
190            if line == "complete" {
191                complete = true;
192                continue;
193            }
194            let section = Section::parse_inherit(line, context, inherit_sections)?.unwrap();
195            sections.add(section)?;
196        }
197
198        Ok(DelinkFile { name, sections, complete, gap: false })
199    }
200
201    pub fn split_file_ext(&self) -> (&str, &str) {
202        self.name.rsplit_once('.').unwrap_or((&self.name, ""))
203    }
204
205    pub fn gap(&self) -> bool {
206        self.gap
207    }
208}
209
210impl Display for DelinkFile {
211    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212        writeln!(f, "{}:", self.name)?;
213        for section in self.sections.sorted_by_address() {
214            writeln!(f, "    {section}")?;
215        }
216        Ok(())
217    }
218}