libdr/
lib.rs

1use std::error;
2use std::fs;
3use std::fs::File;
4use std::io::Read;
5
6extern crate toml_edit;
7use toml_edit::Document;
8
9extern crate reqwest;
10
11extern crate serde_json;
12use serde_json::Value;
13use reqwest::header::USER_AGENT;
14
15extern crate semver;
16use semver::Version;
17use semver::VersionReq;
18
19#[derive(Debug)]
20struct Error(String);
21
22impl error::Error for Error {}
23
24impl std::fmt::Display for Error {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        write!(f, "{}", self.0)
27    }
28}
29
30struct DepRefresh {
31    use_semver:         bool,
32    allow_yanked:       bool,
33    allow_prerelease:   bool,
34}
35
36pub fn update_toml_file(
37    filename: &str,
38    unsafe_file_updates: bool,
39    use_semver: bool,
40    allow_yanked: bool,
41    allow_prerelease: bool,
42) -> Result<(), Box<dyn error::Error>> {
43    println!("Reading file: {}", filename);
44
45    let mut contents = String::new();
46    {
47        let mut f = File::open(filename)?;
48        f.read_to_string(&mut contents)
49            .expect("Something went wrong reading the file.");
50    }
51
52    let dr = DepRefresh {
53        use_semver,
54        allow_yanked,
55        allow_prerelease,
56    };
57    let new_contents = dr.update_toml(&contents)?;
58    if new_contents == contents {
59        return Ok(());
60    }
61
62    if !unsafe_file_updates {
63        let filename_old = filename.to_string() + ".old";
64        let _ = fs::remove_file(&filename_old);
65        fs::copy(filename, filename_old)?;
66    }
67
68    fs::write(filename, new_contents)?;
69    Ok(())
70}
71
72#[test]
73fn test_update_toml_semver() {
74    let toml = r#"
75[package]
76version = "0.1.0"
77
78[dependencies]
79reqwest = { version = "0.10.3", features = ["blocking"] }
80structopt = "0.3"
81
82[dependencies.toml_edit]
83version = "0.1.3"
84
85[build-dependencies]
86autocfg = "1.0.0"
87    "#;
88
89    let expected = r#"
90[package]
91version = "0.1.0"
92
93[dependencies]
94reqwest = { version = "0.11.4", features = ["blocking"] }
95structopt = "0.3"
96
97[dependencies.toml_edit]
98version = "0.2.1"
99
100[build-dependencies]
101autocfg = "1.0.0"
102    "#;
103
104    let dr = DepRefresh {
105        use_semver:         true,
106        allow_yanked:       false,
107        allow_prerelease:   false,
108    };
109    let result = dr.update_toml(toml).unwrap();
110    assert_eq!(result, expected);
111}
112
113#[test]
114fn test_update_toml_exact() {
115    let toml = r#"
116[package]
117version = "0.1.0"
118
119[dependencies]
120reqwest = { version = "0.10.3", features = ["blocking"] }
121structopt = "0.3"
122
123[dependencies.toml_edit]
124version = "0.1.3"
125
126[build-dependencies]
127autocfg = "1.0.0"
128    "#;
129
130    let expected = r#"
131[package]
132version = "0.1.0"
133
134[dependencies]
135reqwest = { version = "0.11.4", features = ["blocking"] }
136structopt = "0.3.23"
137
138[dependencies.toml_edit]
139version = "0.2.1"
140
141[build-dependencies]
142autocfg = "1.0.1"
143    "#;
144
145    let dr = DepRefresh {
146        use_semver:         false,
147        allow_yanked:       false,
148        allow_prerelease:   false,
149    };
150    let result = dr.update_toml(toml).unwrap();
151    assert_eq!(result, expected);
152
153}
154
155impl DepRefresh {
156    fn version_matches(&self,
157                       local_version: &str,
158                       online_version: &str)
159                       -> Result<bool, Box<dyn error::Error>> {
160        if self.use_semver {
161            let local_version_sem = match VersionReq::parse(local_version) {
162                Ok(v) => Ok(v),
163                Err(e) => Err(Box::new(Error(format!("Failed to parse Cargo.toml version '{}': {}",
164                                                     local_version, e)))),
165            }?;
166            let online_version_sem = match Version::parse(online_version) {
167                Ok(v) => Ok(v),
168                Err(e) => Err(Box::new(Error(format!("Failed to parse online version '{}': {}",
169                                                     online_version, e)))),
170            }?;
171            Ok(local_version_sem.matches(&online_version_sem))
172        } else {
173            Ok(*local_version == *online_version)
174        }
175    }
176
177    fn check_version(&self,
178                     updates_crate: &mut Vec<(String, String, String)>,
179                     the_crate: &str,
180                     local_version: &str)
181                     -> Result<(), Box<dyn error::Error>> {
182        let local_version = local_version.trim().to_string();
183        println!("\t\tLocal version:  {}", local_version);
184
185        let online_version = self.lookup_latest_version(the_crate)?;
186        println!("\t\tOnline version: {}", &online_version);
187
188        if !self.version_matches(&local_version, &online_version)? {
189            updates_crate.push((the_crate.to_string(), local_version, online_version));
190        }
191        Ok(())
192    }
193
194    fn update_info(&self,
195                   the_crate: &str,
196                   local_version: &str,
197                   online_version: &str) {
198        println!("\tUpdating: {} {} => {}",
199                 the_crate,
200                 local_version,
201                 online_version);
202    }
203
204    fn update_toml_dep_table(&self,
205                             doc: &mut Document,
206                             table_name: &str)
207                             -> Result<(), Box<dyn error::Error>> {
208        if let Some(table) = &doc[table_name].as_table() {
209            let mut updates_crate = Vec::new();
210            let mut updates_crate_version = Vec::new();
211
212            for (the_crate, item) in table.iter() {
213                println!("\tFound: {}", the_crate);
214
215                if let Some(sub_table) = item.as_table() {
216                    if let Some(value) = sub_table.get("version") {
217                        if let Some(local_version) = value.as_str() {
218                            self.check_version(&mut updates_crate_version, the_crate, local_version)?;
219                        }
220                    }
221                } else if let Some(value) = item.as_value() {
222                    if let Some(local_version) = value.as_str() {
223                        self.check_version(&mut updates_crate, the_crate, local_version)?;
224                    }
225                    else if let Some(inline_table) = value.as_inline_table() {
226                        if let Some(value) = inline_table.get("version") {
227                            if let Some(local_version) = value.as_str() {
228                                self.check_version(&mut updates_crate_version, the_crate, local_version)?;
229                            }
230                        }
231                    } else {
232                        println!("** Error: Can not parse {}", value);
233                    }
234                } else {
235                    println!("** Error: Item '{:?}' is neither table nor value.", item);
236                }
237            }
238
239            for (the_crate, local_version, online_version) in updates_crate {
240                self.update_info(&the_crate, &local_version, &online_version);
241                doc[table_name][&the_crate] = toml_edit::value(online_version);
242            }
243            for (the_crate, local_version, online_version) in updates_crate_version {
244                self.update_info(&the_crate, &local_version, &online_version);
245                doc[table_name][&the_crate]["version"] = toml_edit::value(online_version);
246            }
247        }
248        Ok(())
249    }
250
251    fn update_toml(&self, toml: &str) -> Result<String, Box<dyn error::Error>> {
252        let mut doc = toml.parse::<Document>()?;
253        self.update_toml_dep_table(&mut doc, "dependencies")?;
254        self.update_toml_dep_table(&mut doc, "build-dependencies")?;
255        self.update_toml_dep_table(&mut doc, "dev-dependencies")?;
256        Ok(doc.to_string())
257    }
258
259    fn lookup_latest_version(&self, crate_name: &str) -> Result<String, Box<dyn error::Error>> {
260
261        const NAME: &str = env!("CARGO_PKG_NAME");
262        const VERSION: &str = env!("CARGO_PKG_VERSION");
263        const REPO: &str = env!("CARGO_PKG_REPOSITORY");
264        let user_agent = format!("{} {} ( {} )", NAME, VERSION, REPO);
265
266        let uri = format!("https://crates.io/api/v1/crates/{}", crate_name);
267
268        let client = reqwest::blocking::Client::builder()
269            .gzip(true)
270            .build()?;
271        let http_body = client.get(&uri)
272            .header(USER_AGENT, &user_agent)
273            .send()?
274            .text()?;
275
276        let mut version = None;
277        let json_doc: Value = serde_json::from_str(&http_body)?;
278        if let Some(json_versions) = json_doc["versions"].as_array() {
279            for json_version in json_versions {
280                if let Some(yanked) = json_version["yanked"].as_bool() {
281                    if yanked && !self.allow_yanked {
282                        // Skip this yanked version.
283                        continue;
284                    }
285                }
286                match json_version["num"].as_str() {
287                    Some(version_num) => {
288                        match Version::parse(version_num) {
289                            Ok(version_num_sem) => {
290                                if !version_num_sem.pre.is_empty() && !self.allow_prerelease {
291                                    // Skip this pre-release.
292                                    continue;
293                                }
294                                // Found the latest usable version.
295                                version = Some(version_num_sem.to_string());
296                                // Stop the search here.
297                                break;
298                            },
299                            Err(e) => {
300                                return Err(Box::new(Error(format!(
301                                    "Crates.io json info for '{}' did not include a valid version 'num': {}",
302                                    crate_name, e))));
303                            },
304                        }
305                    },
306                    None => {
307                        return Err(Box::new(Error(format!(
308                            "Crates.io json info for '{}' did not include version 'num'",
309                            crate_name))));
310                    },
311                }
312            }
313        }
314
315        match version {
316            Some(version) => Ok(version),
317            None => {
318                Err(Box::new(Error(format!(
319                    "No usable version found for '{}' on crates.io.",
320                    crate_name))))
321            }
322        }
323    }
324}