cat_dev/mion/cgis/
update.rs

1//! API's for interacting with `/update.cgi`.
2//!
3//! This update page is the "entry-point" for dealing with FW versions, and
4//! updates of the underlying MION board itself. Other CGIs implement the
5//! actual "yes commit I want to update to this version".
6//!
7//! *note: right now only support for getting versions is implemented, as
8//! mentioned the upload flow travels through significantly more pages, and has
9//! more buggy behavior I have chosen to not implement yet. PRs accepted.*
10
11use crate::{
12	errors::NetworkError,
13	mion::{
14		cgis::do_simple_request,
15		proto::cgis::{MionCGIErrors, MionFirmwareVersions},
16	},
17};
18use reqwest::{Body, Client, Method};
19use std::net::Ipv4Addr;
20
21/// The start of the upload forms should always begin with this constant value.
22const FORM_PREFIX: &str = r#"<form method="POST" enctype="multipart/form-data""#;
23/// The end of an HTML Form, for extracting without.
24const FORM_SUFFIX: &str = "</form>";
25/// The version string should always print this static string before printing
26/// the version.
27const VERISON_PREFIX: &str = "Version : ";
28
29/// Perform a get request to `update.cgi` to fetch the current versions.
30///
31/// ## Errors
32///
33/// - If we cannot encode the parameters as a form url encoded.
34/// - If we cannot make the HTTP request.
35/// - If the server does not respond with a 200.
36/// - If we cannot read the body from HTTP.
37/// - If we cannot parse the HTML response.
38pub async fn get_versions(mion_ip: Ipv4Addr) -> Result<MionFirmwareVersions, NetworkError> {
39	get_versions_with_raw_client(&Client::default(), mion_ip).await
40}
41
42/// Perform a get request to `update.cgi` to fetch the current versions, but
43/// with an already existing HTTP client.
44///
45/// ## Errors
46///
47/// - If we cannot encode the parameters as a form url encoded.
48/// - If we cannot make the HTTP request.
49/// - If the server does not respond with a 200.
50/// - If we cannot read the body from HTTP.
51/// - If we cannot parse the HTML response.
52pub async fn get_versions_with_raw_client(
53	client: &Client,
54	mion_ip: Ipv4Addr,
55) -> Result<MionFirmwareVersions, NetworkError> {
56	let body_as_string = do_simple_request::<Body>(
57		client,
58		Method::GET,
59		format!("http://{mion_ip}/update.cgi"),
60		None,
61		None,
62	)
63	.await?;
64
65	let (fw_version, fpga_version) = parse_versions_from_update_html(&body_as_string)?;
66	Ok(MionFirmwareVersions::from_versions(
67		fw_version,
68		fpga_version,
69	))
70}
71
72fn parse_versions_from_update_html(body_as_string: &str) -> Result<([u8; 3], u32), MionCGIErrors> {
73	// Parse out the `<body>` tag, we can't close the actual body tag because
74	// _sometimes_ the MION will add in custom colors.
75	let start_tag_location = body_as_string
76		.find("<body")
77		.map(|num| num + 5)
78		.ok_or_else(|| MionCGIErrors::HtmlResponseMissingBody(body_as_string.to_owned()))?;
79	let body_without_start_tag = body_as_string.split_at(start_tag_location).1;
80	let end_tag_location = body_without_start_tag
81		.find("</body>")
82		.ok_or_else(|| MionCGIErrors::HtmlResponseMissingBody(body_as_string.to_owned()))?;
83	let body_contents = body_without_start_tag
84		.split_at(end_tag_location)
85		.0
86		.to_owned();
87	let versions = get_version_elements(&body_contents)?;
88	// We should get an FPGA version, and a FW version. IPL versions are tracked
89	// elsewhere.
90	if versions.len() != 2 {
91		return Err(MionCGIErrors::HtmlResponseMissingVersions(versions));
92	}
93
94	let fpga_version = u32::from_str_radix(&versions[0], 16)
95		.map_err(MionCGIErrors::HtmlResponseNumberExpectedButNotThere)?;
96	let mut fw_version = [0_u8; 3];
97	for (idx, item) in versions[1].splitn(3, '.').enumerate() {
98		fw_version[idx] = item
99			.parse::<u8>()
100			.map_err(MionCGIErrors::HtmlResponseNumberExpectedButNotThere)?;
101	}
102
103	Ok((fw_version, fpga_version))
104}
105
106/// Parse all of the version strings out of an HTML response.
107fn get_version_elements(mut html_response: &str) -> Result<Vec<String>, MionCGIErrors> {
108	let mut results = Vec::new();
109
110	while let Some(idx) = html_response.find(FORM_PREFIX) {
111		// This techincally doesn't cut off the ending of the `<form>` tag! This is
112		// expected because certain firmwares in certain states can choose to add
113		// in color fields, and sometimes not.
114		//
115		// Prettyness gets in the way of our "parsing", if you can even call it
116		// that.
117		let missing_form_start = html_response.split_at(idx + FORM_PREFIX.len()).1;
118		let Some(form_close_at) = missing_form_start.find(FORM_SUFFIX) else {
119			return Err(MionCGIErrors::HtmlResponseMissingClosingTag(
120				FORM_SUFFIX.to_owned(),
121				missing_form_start.to_owned(),
122			));
123		};
124		let (form_insides, form_outsides) = missing_form_start.split_at(form_close_at);
125
126		// Skip past the `</form>` we just found for future loops.
127		html_response = &form_outsides[FORM_SUFFIX.len()..];
128
129		// Remove any HTML elements from the form, we only care about the text
130		// at the beginning.
131		let version_data = if let Some(loc) = form_insides.find("<input") {
132			form_insides.split_at(loc).0.trim().replace("<br>", "")
133		} else {
134			form_insides.trim().replace("<br>", "")
135		};
136		let Some(version_prefix_location) = version_data.find(VERISON_PREFIX) else {
137			return Err(MionCGIErrors::HtmlResponseMissingVersionPart(
138				VERISON_PREFIX.to_owned(),
139				version_data,
140			));
141		};
142
143		let version_with_extra_data = version_data
144			.split_at(version_prefix_location + VERISON_PREFIX.len())
145			.1;
146		let Some(version_suffix_location) = version_with_extra_data.find(')') else {
147			return Err(MionCGIErrors::HtmlResponseMissingVersionPart(
148				")".to_owned(),
149				version_with_extra_data.to_owned(),
150			));
151		};
152		results.push(
153			version_with_extra_data
154				.split_at(version_suffix_location)
155				.0
156				.trim()
157				.to_owned(),
158		);
159	}
160
161	Ok(results)
162}
163
164#[cfg(test)]
165mod unit_tests {
166	use super::*;
167
168	/// The response for a real life update page, minus some HTML color stringt hashtags
169	/// to make the string easier to handle in Rust.
170	const REAL_LIFE_UPDATE_PAGE: &str = r#"<HTML>
171<head>
172<meta http-equiv="Content-Type" content="text/html; charset=ASCII">
173<meta http-equiv="Pragma" content="no-cache"><meta http-equiv="Cache-Control" content="no-cache"><meta http-equiv="Expires" content="0"><title>Program Update</title>
174<script type="text/javascript" src="pwiss.js"></script>
175</head>
176<body bgcolor="FFFFFF" text="000000">
177<div align="center">
178<h1>Program Update</h1><br><br>
179<div id="disp1">
180<table border=0 cellspacing=0 cellpadding=0>
181<tr>
182<td>
183<form method="POST" enctype="multipart/form-data" action="update_fpga.cgi">
184FPGA Data&nbsp;(Present FPGA Version : 13052071)<br>
185<input type="hidden" name="func" value="fpga_upd">
186<input type="file" name="filename" size=50>
187<input type="submit" value="Upload" OnClick="disp_change();">
188<br>
189</form>
190<form method="POST" enctype="multipart/form-data" action="update_fw.cgi">
191Firmware&nbsp;(Present Firmware Version : 00.14.70)<br>
192<input type="hidden" name="func" value="fw_upd">
193<input type="file" name="filename" size=50>
194<input type="submit" value="Upload" OnClick="disp_change();">
195<br>
196</form>
197</td>
198</tr>
199</table>
200</div>
201<div id="disp2" style="display:none;">
202  File Uploading . . .<br>
203<br>
204</div>
205</div>
206<hr>
207<a href="./">Homepage</a>
208</body>
209</HTML>"#;
210
211	#[test]
212	pub fn can_parse_update_html() {
213		let (fw_version, fpga_verison) = parse_versions_from_update_html(REAL_LIFE_UPDATE_PAGE)
214			.expect("Failed to parse versions out of a 0.0.14.70 update.cgi page!");
215		assert_eq!(
216			fw_version,
217			[0_u8, 14_u8, 70_u8],
218			"Firmware Version did not match expected!"
219		);
220		assert_eq!(
221			fpga_verison, 0x13052071,
222			"FPGA version did not match expected!",
223		);
224	}
225}