rust_toolchain_file/
legacy.rs

1use std::path::{Path, PathBuf};
2use std::str::FromStr;
3
4#[cfg(test)]
5mod tests;
6
7/// A parser for the legacy toolchain file format.
8pub struct Parser<'content> {
9    content: &'content str,
10
11    /// According to the rustup book, the legacy format must be encoded as US-ASCII without BOM.
12    ///
13    /// Setting this field to true, we'll be more lenient, and allow the input to be encoded as UTF-8.
14    strict: bool,
15}
16
17impl<'content> Parser<'content> {
18    /// Initialize a parser which leniently accepts the US-ASCII compatible UTF-8 encoding.
19    pub fn new(content: &'content str) -> Self {
20        Self {
21            content,
22            strict: false,
23        }
24    }
25
26    /// Initialize a parser, which strictly only accepts US-ASCII encoded content.
27    pub fn strict(content: &'content str) -> Self {
28        Self {
29            content,
30            strict: true,
31        }
32    }
33}
34
35impl Parser<'_> {
36    pub fn parse(&self) -> Result<LegacyToolchainFile, ParserError> {
37        // Verify the required encoding.
38        if self.strict && !self.content.is_ascii() {
39            return Err(ParserError::InvalidEncodingStrict);
40        }
41
42        let content = self.content.trim();
43
44        // Verify, that there is content
45        if content.is_empty() {
46            return Err(ParserError::IsEmpty);
47        }
48
49        // Verify the contents consist of one specifier or path, on a single line
50        let line_count = content.lines().count();
51
52        if line_count != 1 {
53            return Err(ParserError::TooManyLines(line_count));
54        }
55
56        // Set the channel type
57        let channel = if Path::new(content).is_absolute() {
58            LegacyChannel::Path(content.into())
59        } else {
60            LegacyChannel::Spec(content.to_string())
61        };
62
63        Ok(LegacyToolchainFile { channel })
64    }
65}
66
67#[derive(Debug, thiserror::Error, PartialEq)]
68pub enum ParserError {
69    #[error("Unable to parse legacy toolchain file: toolchain file was empty")]
70    IsEmpty,
71
72    #[error("Encountered invalid encoding while parsing legacy rust-toolchain file. The expected encoding to be US-ASCII, and lenient encoding was disabled.")]
73    InvalidEncodingStrict,
74
75    #[error("Expected a single line containing the toolchain specifier but found '{0}' lines.")]
76    TooManyLines(usize),
77}
78
79/// The legacy toolchain file variant
80#[derive(Debug, PartialEq)]
81pub struct LegacyToolchainFile {
82    channel: LegacyChannel,
83}
84
85impl LegacyToolchainFile {
86    pub fn new(channel: LegacyChannel) -> Self {
87        Self { channel }
88    }
89}
90
91impl FromStr for LegacyToolchainFile {
92    type Err = ParserError;
93
94    fn from_str(content: &str) -> Result<Self, Self::Err> {
95        let parser = Parser::new(content);
96
97        parser.parse()
98    }
99}
100
101impl LegacyToolchainFile {
102    pub fn channel(&self) -> &LegacyChannel {
103        &self.channel
104    }
105
106    /// Return the toolchain path, given that the toolchain-file contents
107    /// consists of a path and not a channel specification.
108    pub fn path(&self) -> Option<&Path> {
109        match self.channel {
110            LegacyChannel::Path(ref p) => Some(p.as_path()),
111            _ => None,
112        }
113    }
114
115    /// Return the channel specification specified in the toolchain file, given that the toolchain-file
116    /// contents consists of a channel spec and not a path.
117    pub fn spec(&self) -> Option<&str> {
118        match self.channel {
119            LegacyChannel::Spec(ref p) => Some(p.as_str()),
120            _ => None,
121        }
122    }
123}
124
125/// The channel specified within the legacy toolchain file.
126#[derive(Debug, PartialEq)]
127pub enum LegacyChannel {
128    Path(PathBuf),
129    Spec(String),
130}