5b6c2dfea5c22320fbb5b93b19272111a88e72ea
[pspp] / rust / build.rs
1 use anyhow::{anyhow, Result as AnyResult};
2 use std::{
3     collections::{BTreeMap, HashSet, VecDeque},
4     env::var_os,
5     fs::{read_to_string, File},
6     io::{Error as IoError, Write},
7     path::{Path, PathBuf},
8 };
9
10 #[derive(Copy, Clone, PartialEq, Eq, Ord, PartialOrd)]
11 enum Source {
12     CP,
13     IBM,
14     Windows,
15 }
16
17 // Code page number.
18 type CPNumber = usize;
19
20 fn process_converter<'a>(
21     fields: &Vec<&'a str>,
22     codepages: &mut BTreeMap<CPNumber, BTreeMap<Source, Vec<&'a str>>>,
23 ) {
24     if fields.is_empty() || fields[0] == "{" {
25         return;
26     }
27
28     let mut cps: BTreeMap<Source, CPNumber> = BTreeMap::new();
29     let mut iana = VecDeque::new();
30     let mut other = VecDeque::new();
31
32     let mut iter = fields.iter().peekable();
33     while let Some(&name) = iter.next() {
34         if iter.next_if(|&&s| s == "{").is_some() {
35             let mut standards = HashSet::new();
36             loop {
37                 let &standard = iter.next().expect("missing `}` in list of standards");
38                 if standard == "}" {
39                     break;
40                 }
41                 standards.insert(standard);
42             }
43
44             if standards.contains("IANA*") {
45                 iana.push_front(name);
46             } else if standards.contains("IANA") {
47                 iana.push_back(name);
48             } else if standards.iter().any(|&s| s.ends_with('*')) {
49                 other.push_front(name);
50             } else {
51                 other.push_back(name);
52             }
53         } else {
54             // Untagged names are completely nonstandard.
55             continue;
56         }
57
58         if let Some(number) = name.strip_prefix("cp") {
59             if let Ok(number) = number.parse::<CPNumber>() {
60                 cps.insert(Source::CP, number);
61             }
62         }
63
64         if let Some(number) = name.strip_prefix("windows-") {
65             if let Ok(number) = number.parse::<CPNumber>() {
66                 cps.insert(Source::Windows, number);
67             }
68         }
69
70         if let Some(number) = name.strip_prefix("ibm-") {
71             if let Ok(number) = number.parse::<CPNumber>() {
72                 cps.insert(Source::IBM, number);
73             }
74         }
75     }
76
77     // If there are no tagged names then this is completely nonstandard.
78     if iana.is_empty() && other.is_empty() {
79         return;
80     }
81
82     let all: Vec<&str> = iana.into_iter().chain(other.into_iter()).collect();
83     for (source, number) in cps {
84         codepages
85             .entry(number)
86             .or_insert_with(BTreeMap::new)
87             .insert(source, all.clone());
88     }
89 }
90
91 fn write_output(
92     codepages: &BTreeMap<CPNumber, BTreeMap<Source, Vec<&str>>>,
93     file_name: &PathBuf,
94 ) -> Result<(), IoError> {
95     let mut file = File::create(file_name)?;
96
97     write!(file, "{}", "\
98 use lazy_static::lazy_static;
99 use std::collections::HashMap;
100
101 lazy_static! {
102     static ref CODEPAGE_NUMBER_TO_NAME: HashMap<u32, &'static str> = {
103         let mut map = HashMap::new();
104 ")?;
105
106     for (&cpnumber, value) in codepages.iter() {
107         let source = value.keys().max().unwrap();
108         let name = value[source][0];
109         writeln!(file, "        map.insert({cpnumber}, \"{name}\");")?;
110     }
111     write!(file, "{}", "\
112         map
113     };
114 }
115 ")?;
116
117     let mut names: BTreeMap<&str, BTreeMap<Source, Vec<CPNumber>>> = BTreeMap::new();
118     for (&cpnumber, value) in codepages.iter() {
119         for (&source, value2) in value.iter() {
120             for &name in value2.iter() {
121                 names
122                     .entry(name)
123                     .or_insert_with(BTreeMap::new)
124                     .entry(source)
125                     .or_insert_with(Vec::new)
126                     .push(cpnumber);
127             }
128         }
129     }
130
131     for (&name, value) in names.iter() {
132         for (_source, numbers) in value.iter().rev() {
133             println!("  {{ {}, \"{name}\" }},", numbers[0]);
134             break;
135         }
136     }
137
138     Ok(())
139 }
140
141 fn main() -> AnyResult<()> {
142     println!("cargo:rerun-if-changed=build.rs");
143
144     let input_file = Path::new(env!("CARGO_MANIFEST_DIR")).join("../src/data/convrtrs.txt");
145     println!("cargo:rerun-if-changed={}", input_file.to_string_lossy());
146     let input = read_to_string(&input_file)
147         .map_err(|e| anyhow!("{}: read failed ({e})", input_file.to_string_lossy()))?;
148
149     let mut codepages: BTreeMap<CPNumber, BTreeMap<Source, Vec<&str>>> = BTreeMap::new();
150     let mut converter: Vec<&str> = Vec::new();
151     for line in input.lines() {
152         let line = line
153             .find('#')
154             .map(|position| &line[..position])
155             .unwrap_or(line)
156             .trim_end();
157         if !line.starts_with(&[' ', '\t']) {
158             process_converter(&converter, &mut codepages);
159             converter.clear();
160         }
161         converter.extend(line.split_whitespace());
162     }
163     process_converter(&converter, &mut codepages);
164
165     let output_file_name = Path::new(&var_os("OUT_DIR").unwrap()).join("encodings.rs");
166
167     write_output(&codepages, &output_file_name)
168         .map_err(|e| anyhow!("{}: write failed ({e})", output_file_name.to_string_lossy()))?;
169
170     Ok(())
171 }