baaacc5d4f9edcee5a46c27448a88b1298cffbae
[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     Codepage,
13     Ibm,
14     Windows,
15 }
16
17 // Code page number.
18 type CodepageNumber = usize;
19
20 fn process_converter<'a>(
21     fields: &Vec<&'a str>,
22     codepages: &mut BTreeMap<CodepageNumber, BTreeMap<Source, Vec<&'a str>>>,
23 ) {
24     if fields.is_empty() || fields[0] == "{" {
25         return;
26     }
27
28     let mut cps: BTreeMap<Source, CodepageNumber> = 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::<CodepageNumber>() {
60                 cps.insert(Source::Codepage, number);
61             }
62         }
63
64         if let Some(number) = name.strip_prefix("windows-") {
65             if let Ok(number) = number.parse::<CodepageNumber>() {
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::<CodepageNumber>() {
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).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<CodepageNumber, BTreeMap<Source, Vec<&str>>>,
93     file_name: &PathBuf,
94 ) -> Result<(), IoError> {
95     let mut file = File::create(file_name)?;
96
97     file.write_all(
98         "\
99 use lazy_static::lazy_static;
100 use std::collections::HashMap;
101
102 lazy_static! {
103     static ref CODEPAGE_NUMBER_TO_NAME: HashMap<i32, &'static str> = {
104         let mut map = HashMap::new();
105 "
106         .as_bytes(),
107     )?;
108
109     for (&cpnumber, value) in codepages.iter() {
110         let source = value.keys().max().unwrap();
111         let name = value[source][0];
112         writeln!(file, "        map.insert({cpnumber}, \"{name}\");")?;
113     }
114     file.write_all(
115         "        map
116     };
117
118     static ref CODEPAGE_NAME_TO_NUMBER: HashMap<&'static str, u32> = {
119         let mut map = HashMap::new();
120 "
121         .as_bytes(),
122     )?;
123
124     let mut names: BTreeMap<String, BTreeMap<Source, Vec<CodepageNumber>>> = BTreeMap::new();
125     for (&cpnumber, value) in codepages.iter() {
126         for (&source, value2) in value.iter() {
127             for name in value2.iter().map(|name| name.to_ascii_lowercase()) {
128                 names
129                     .entry(name)
130                     .or_insert_with(BTreeMap::new)
131                     .entry(source)
132                     .or_insert_with(Vec::new)
133                     .push(cpnumber);
134             }
135         }
136     }
137
138     for (name, value) in names.iter() {
139         for (_source, numbers) in value.iter().rev().take(1) {
140             writeln!(file, "        map.insert(\"{name}\", {});", numbers[0])?;
141         }
142     }
143     file.write_all(
144         "        map
145     };
146 }
147 "
148         .as_bytes(),
149     )?;
150
151     Ok(())
152 }
153
154 fn main() -> AnyResult<()> {
155     println!("cargo:rerun-if-changed=build.rs");
156
157     let input_file = Path::new(env!("CARGO_MANIFEST_DIR")).join("convrtrs.txt");
158     println!("cargo:rerun-if-changed={}", input_file.to_string_lossy());
159     let input = read_to_string(&input_file)
160         .map_err(|e| anyhow!("{}: read failed ({e})", input_file.to_string_lossy()))?;
161
162     let mut codepages: BTreeMap<CodepageNumber, BTreeMap<Source, Vec<&str>>> = BTreeMap::new();
163     let mut converter: Vec<&str> = Vec::new();
164     for line in input.lines() {
165         let line = line
166             .find('#')
167             .map(|position| &line[..position])
168             .unwrap_or(line)
169             .trim_end();
170         if !line.starts_with([' ', '\t']) {
171             process_converter(&converter, &mut codepages);
172             converter.clear();
173         }
174         converter.extend(line.split_whitespace());
175     }
176     process_converter(&converter, &mut codepages);
177
178     let output_file_name = Path::new(&var_os("OUT_DIR").unwrap()).join("encodings.rs");
179
180     write_output(&codepages, &output_file_name)
181         .map_err(|e| anyhow!("{}: write failed ({e})", output_file_name.to_string_lossy()))?;
182
183     Ok(())
184 }