1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
use crate::{depinfo::DependencyInfo, input::InputFile, output};
use cargo::core::Shell;
use memchr::{memchr2, memmem};
use std::{cell::RefMut, io, process::ExitCode};

pub enum Check {
	/// Everything is up to date.
	UpToDate,

	/// The dependency info is malformed.
	InvalidDepInfo(anyhow::Error),

	/// The input (template or rustdoc) have changed.
	InputChanged,

	/// One or more dependencies use an incompatible version.
	IncompatibleVersion(String),

	/// The readme used an outdated "markdown version".
	OutdatedMarkdown,

	/// Input and output are different (no dep info was included).
	OutputChanged
}

impl Check {
	pub fn print(&self, mut shell: RefMut<'_, Shell>) {
		match self {
			Check::UpToDate => shell.note("Readme is up to date"),
			Check::InvalidDepInfo(e) => {
				shell.error(format!("Readme has invalid dependency info: {e}"))
			},
			Check::InputChanged => shell.error("Input has changed"),
			Check::IncompatibleVersion(name) => shell.error(format!(
				"Readme links to incompatible version of dependency `{name}`"
			)),
			Check::OutdatedMarkdown => {
				shell.error("The readme was created with an outdated version of this tool")
			},
			Check::OutputChanged => shell.error("Readme has changed")
		}
		.ok();
	}
}

impl From<Check> for ExitCode {
	fn from(check: Check) -> Self {
		match check {
			Check::UpToDate => Self::SUCCESS,
			_ => Self::FAILURE
		}
	}
}

pub fn check_up2date(
	mut shell: RefMut<'_, Shell>,
	input: InputFile,
	template: &str,
	check_file: &mut dyn io::Read
) -> anyhow::Result<Check> {
	let mut check_buf = Vec::new();
	check_file.read_to_end(&mut check_buf)?;

	let search_key = b" [__cargo_doc2readme_dependencies_info]: ";
	if let Some(search_idx) = memmem::find(&check_buf, search_key) {
		let sub = &check_buf[search_idx + search_key.len()..];
		let end_idx = memchr2(b' ', b'\n', sub).unwrap_or(sub.len());
		let depinfo_str = String::from_utf8(sub[..end_idx].to_vec()).unwrap();
		let depinfo = match DependencyInfo::decode(depinfo_str) {
			Ok(depinfo) => depinfo,
			Err(e) => {
				return Ok(Check::InvalidDepInfo(e));
			}
		};

		// ensure markdown version matches
		if depinfo.check_outdated() {
			return Ok(Check::OutdatedMarkdown);
		}

		// ensure the input is up to date
		if !depinfo.check_input(template, &input.rustdoc) {
			return Ok(Check::InputChanged);
		}

		// ensure that the dependencies that were used in the readme still meet the current required
		// versions. dependencies that are missing in the readme don't matter.
		for (lib_name, dep) in &input.dependencies {
			shell
				.verbose(|shell| {
					shell.status("Checking", format!("{} = \"{}\"", dep.crate_name, dep.req))
				})
				.ok();
			if !depinfo.check_dependency(&dep.crate_name, Some(&dep.req), lib_name, true) {
				return Ok(Check::IncompatibleVersion(dep.crate_name.clone()));
			}
		}

		// looks like everything is up to date
		return Ok(Check::UpToDate);
	}

	// if no dependency info was available, do a bytewise comparison
	let mut output_buf = Vec::new();
	output::emit(input, template, &mut output_buf)?;
	Ok(if output_buf == check_buf {
		Check::UpToDate
	} else {
		Check::OutputChanged
	})
}