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 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 continue;
293 }
294 version = Some(version_num_sem.to_string());
296 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}