Skip to content

Commit 716c59e

Browse files
authored
feat: Add UEFI HTTP boot support (#121)
* feat: 增加 Riscv64 架构支持 * feat: add UEFI HTTP boot support with file upload and manifest APIs * feat: add UEFI HTTP Boot runner with CLI and config support * feat: support efi_loader_path config for HTTP Boot runner * feat: add httpboot-loader crate with no-std manifest parser and UEFI stub * feat: extract URI from UEFI device path and derive sibling manifest URL * feat: add downloaded manifest byte parser with validation * feat: probe EFI HTTP protocol availability via locate_handle_buffer * feat: probe EFI HTTP child handle creation and destruction * feat: probe EFI HTTP configure and reset with IPv4 default settings * feat: refactor UEFI types into modules and implement HTTP GET request for manifest * feat: implement HTTP response handling and manifest parsing for UEFI loader * feat: implement kernel HTTP probe request and response handling for UEFI loader * fix: rename httpboot-loader crate to httpboot * feat: implement chunked kernel download to UEFI pool memory with checksum * feat: download kernel to fixed load address using UEFI allocate_pages * feat: probe memory map and prepare kernel jump readiness for UEFI loader * feat: implement ExitBootServices call with retry and memory map abstraction for UEFI loader * feat: add entry call module with arch-aware calling convention for UEFI loader * feat: integrate entry point call into ExitBootServices flow for UEFI loader * feat: add proxy DHCP service support for HTTP Boot * feat: extend proxy DHCP to handle DHCP Request/ACK with configurable network parameters * feat: wire up full HTTP boot loader pipeline with boot-jump feature gate and arch validation * feat: support pre-built frontend assets via EMBED_WEB_DIR env * feat: add HTTP range request support for chunked kernel download with retry logic * feat: add COM1 serial port output to UEFI console module * feat: pass boot info with conventional memory regions to kernel entry point * refactor: streamline UEFI HTTP boot console output and add retry loop * feat: hide cursor during kernel download progress and add remaining TextOutput ABI fields * docs: add comment explaining ASUS NUC15 firmware HTTP range chunk size * refactor: adapt HTTP Boot runner to main tool context * refactor: Remove virtual power management and rename uefi_http to httpboot * docs: update httpboot README.md * fix: remove proxy_dhcp that is no longer needed. * fix: remove mac address and client ip * fix: resolve error and warning in ci * fix(ostool-server): improve install and update scripts * fix: refine httpboot uefi retry and console helpers * refactpr: refactor HTTP Boot Module and Implement Artifact Upload * feat: refactor http_boot_url to prioritize configured public base URL and add tests * delete: remove Asus NUC15 board config test case * feat: implement artifact upload in HTTP Boot session and remove manifest handling * feat: add LoaderDiscovery strategy to UEFI HTTP profiles - Introduced a new UEFI HTTP strategy, LoaderDiscovery, to the configuration. - Updated UefiHttpProfile to include a mac field and set default values for new fields. - Modified tests to reflect changes in UEFI HTTP profile structure and strategy. - Implemented LoaderRegistry for managing loader registrations and offers. - Updated the application state to include the LoaderRegistry. - Adjusted web UI types and views to accommodate new strategy and MAC address input. * feat: add support for HTTP Boot kernel upload in BoardServerClient * fix: update author information and refactor serial read configurations * feat: implement HTTP Boot discovery service with configuration and UDP handling * Refactor HTTP Boot implementation by removing manifest and artifact upload endpoints - Updated README to reflect changes in HTTP Boot file storage. - Removed `HttpBootManifest` and `HttpBootArtifactRequest` from API models and routes. - Cleaned up related code in router and tests, ensuring no references to removed endpoints. - Adjusted `UefiHttpProfile` structure to eliminate unnecessary fields. - Updated HTTP Boot configuration to remove loader and kernel parameters. - Refactored tests to align with the new HTTP Boot flow, focusing on kernel file uploads only. - Ensured all related components are consistent with the removal of the manifest-based HTTP Boot flow. * feat: implement HTTP Boot discovery loader design and remove legacy strategy * feat: update HTTP Boot configuration to allow optional MAC address and improve board matching logic * refactor: simplify error message formatting and improve string interpolation across multiple files * refactor: remove old HTTP Boot support and related configurations from the build system * feat: add zerocopy dependency with specific version to maintain compatibility * fix: fix error message and fmt check.yaml * feat: implement HTTP Boot support with serial communication and message handling * fix(deps): make ostool usable as tgoskits git dependency * feat(httpboot): simplify board web config * Refactor HTTP Boot functionality by removing discovery and loader modules - Removed the HTTP Boot discovery service and related configuration from `HttpBootConfig`. - Deleted the `loaders` module and its associated logic for managing loader registrations and offers. - Introduced a new `publish` module to handle kernel publishing functionality. - Updated the router to remove routes related to loader hello requests and boot offers. - Adjusted the `put_http_boot_kernel` function to utilize the new publishing mechanism. - Modified tests to reflect changes in the HTTP Boot workflow, focusing on kernel uploads. * feat: enhance HTTP client configuration and update public API tests * feat(httpboot): increase READY_WAIT_TIMEOUT and implement gradual serial line writing * feat(httpboot): enhance file reading for HTTP Boot with range support * feat(httpboot): refactor boot offer handling and introduce LoaderReadyMonitor * feat(httpboot): simplify boot offer line writing by removing delay and refactoring function * refactor(httpboot): streamline terminal run options and improve panic message formatting * feat(httpboot): enhance serial output logging for loader readiness * feat(httpboot): refactor HTTP Boot structures and improve serial message handling * feat(httpboot): refactor HTTP Boot handling to use session-based file management and remove obsolete directory creation * feat(httpboot): add boot_arch field to board editor and support "other" arch in kernel upload
1 parent f5e0580 commit 716c59e

49 files changed

Lines changed: 2896 additions & 959 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 365 additions & 339 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
[workspace]
22
exclude = ["assets/*", "fit_test"]
3-
members = ["ostool", "uboot-shell", "jkconfig", "fitimage", "ostool-server"]
3+
members = [
4+
"ostool",
5+
"uboot-shell",
6+
"jkconfig",
7+
"fitimage",
8+
"ostool-server",
9+
"httpboot-protocol",
10+
]
411
resolver = "3"
512

613
[workspace.dependencies]
@@ -15,6 +22,8 @@ serde_json = "1"
1522

1623
toml = "1.0"
1724

25+
httpboot-protocol = { path = "httpboot-protocol" }
26+
1827
# Error handling
1928
thiserror = "2"
2029

fitimage/src/compression/gzip.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,11 @@ impl CompressionInterface for GzipCompressor {
6868
let mut encoder = GzEncoder::new(Vec::new(), self.get_compression_level());
6969

7070
encoder.write_all(data).map_err(|e| {
71-
crate::error::MkImageError::compression_error(format!("Gzip compression failed: {}", e))
71+
crate::error::MkImageError::compression_error(format!("Gzip compression failed: {e}"))
7272
})?;
7373

7474
encoder.finish().map_err(|e| {
75-
crate::error::MkImageError::compression_error(format!("Gzip finish failed: {}", e))
75+
crate::error::MkImageError::compression_error(format!("Gzip finish failed: {e}"))
7676
})
7777
}
7878

@@ -86,10 +86,7 @@ impl CompressionInterface for GzipCompressor {
8686
let mut buffer = Vec::new();
8787

8888
decoder.read_to_end(&mut buffer).map_err(|e| {
89-
crate::error::MkImageError::compression_error(format!(
90-
"Gzip decompression failed: {}",
91-
e
92-
))
89+
crate::error::MkImageError::compression_error(format!("Gzip decompression failed: {e}"))
9390
})?;
9491

9592
Ok(buffer)

fitimage/src/error.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,13 @@ impl MkImageError {
122122

123123
impl From<flate2::CompressError> for MkImageError {
124124
fn from(err: flate2::CompressError) -> Self {
125-
Self::compression_error(format!("Gzip compression error: {}", err))
125+
Self::compression_error(format!("Gzip compression error: {err}"))
126126
}
127127
}
128128

129129
impl From<flate2::DecompressError> for MkImageError {
130130
fn from(err: flate2::DecompressError) -> Self {
131-
Self::compression_error(format!("Gzip decompression error: {}", err))
131+
Self::compression_error(format!("Gzip decompression error: {err}"))
132132
}
133133
}
134134

httpboot-protocol/Cargo.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
authors = ["柏乔森 <baiqiaosen@gmail.com>"]
3+
categories = ["embedded", "development-tools"]
4+
description = "Shared HTTP Boot protocol types and parsers"
5+
edition = "2024"
6+
keywords = ["httpboot", "uefi", "bootloader"]
7+
license = "MIT OR Apache-2.0"
8+
name = "httpboot-protocol"
9+
repository = "https://github.com/drivercraft/ostool"
10+
version = "0.1.0"
11+
12+
[features]
13+
default = ["std", "serde"]
14+
serde = ["dep:serde", "dep:serde_json"]
15+
std = []
16+
17+
[dependencies]
18+
serde = { workspace = true, features = ["derive"], optional = true }
19+
serde_json = { workspace = true, optional = true }

httpboot-protocol/src/lib.rs

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#![cfg_attr(not(feature = "std"), no_std)]
2+
3+
pub const SERIAL_PROTOCOL_VERSION: u16 = 1;
4+
pub const SERIAL_READY_PREFIX: &str = "AXLOADER READY ";
5+
pub const SERIAL_BOOT_PREFIX: &str = "AXLOADER BOOT ";
6+
7+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
10+
pub enum BootArch {
11+
X86_64,
12+
Aarch64,
13+
Loongarch64,
14+
Riscv64,
15+
Other,
16+
}
17+
18+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20+
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
21+
pub enum ImageFormat {
22+
Elf64,
23+
}
24+
25+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27+
pub struct SerialReadyMessage<'a> {
28+
pub protocol_version: u16,
29+
pub board: &'a str,
30+
pub arch: BootArch,
31+
pub loader_version: Option<&'a str>,
32+
}
33+
34+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
35+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36+
pub struct SerialBootOfferMessage<'a> {
37+
pub protocol_version: u16,
38+
pub boot_id: &'a str,
39+
pub kernel_url: &'a str,
40+
pub kernel_size: u64,
41+
pub image_format: ImageFormat,
42+
pub arch: BootArch,
43+
pub entry_symbol: Option<&'a str>,
44+
}
45+
46+
#[cfg(feature = "std")]
47+
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
48+
#[derive(Debug, Clone, PartialEq, Eq)]
49+
pub struct KernelPublishResponse {
50+
pub boot_id: String,
51+
pub kernel_url: String,
52+
pub kernel_size: u64,
53+
pub kernel_sha256: Option<String>,
54+
}
55+
56+
#[cfg(all(feature = "std", feature = "serde"))]
57+
#[derive(Debug)]
58+
pub enum SerialMessageError {
59+
InvalidPrefix(&'static str),
60+
Json(serde_json::Error),
61+
}
62+
63+
#[cfg(all(feature = "std", feature = "serde"))]
64+
impl core::fmt::Display for SerialMessageError {
65+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
66+
match self {
67+
Self::InvalidPrefix(prefix) => {
68+
write!(
69+
f,
70+
"serial line does not start with expected prefix `{prefix}`"
71+
)
72+
}
73+
Self::Json(err) => write!(f, "failed to parse serial message JSON: {err}"),
74+
}
75+
}
76+
}
77+
78+
#[cfg(all(feature = "std", feature = "serde"))]
79+
impl std::error::Error for SerialMessageError {}
80+
81+
#[cfg(all(feature = "std", feature = "serde"))]
82+
impl From<serde_json::Error> for SerialMessageError {
83+
fn from(err: serde_json::Error) -> Self {
84+
Self::Json(err)
85+
}
86+
}
87+
88+
#[cfg(all(feature = "std", feature = "serde"))]
89+
pub fn render_serial_ready(message: &SerialReadyMessage<'_>) -> Result<String, serde_json::Error> {
90+
Ok(format!(
91+
"{SERIAL_READY_PREFIX}{}",
92+
serde_json::to_string(message)?
93+
))
94+
}
95+
96+
#[cfg(all(feature = "std", feature = "serde"))]
97+
pub fn parse_serial_ready(line: &str) -> Result<SerialReadyMessage<'_>, SerialMessageError> {
98+
let body = line
99+
.trim()
100+
.strip_prefix(SERIAL_READY_PREFIX)
101+
.ok_or(SerialMessageError::InvalidPrefix(SERIAL_READY_PREFIX))?;
102+
Ok(serde_json::from_str(body)?)
103+
}
104+
105+
#[cfg(all(feature = "std", feature = "serde"))]
106+
pub fn render_serial_boot_offer(
107+
message: &SerialBootOfferMessage<'_>,
108+
) -> Result<String, serde_json::Error> {
109+
Ok(format!(
110+
"{SERIAL_BOOT_PREFIX}{}",
111+
serde_json::to_string(message)?
112+
))
113+
}
114+
115+
#[cfg(all(feature = "std", feature = "serde"))]
116+
pub fn parse_serial_boot_offer(
117+
line: &str,
118+
) -> Result<SerialBootOfferMessage<'_>, SerialMessageError> {
119+
let body = line
120+
.trim()
121+
.strip_prefix(SERIAL_BOOT_PREFIX)
122+
.ok_or(SerialMessageError::InvalidPrefix(SERIAL_BOOT_PREFIX))?;
123+
Ok(serde_json::from_str(body)?)
124+
}
125+
126+
#[cfg(test)]
127+
mod tests {
128+
use super::{
129+
BootArch, ImageFormat, SERIAL_BOOT_PREFIX, SERIAL_PROTOCOL_VERSION, SERIAL_READY_PREFIX,
130+
SerialBootOfferMessage, SerialReadyMessage, parse_serial_boot_offer, parse_serial_ready,
131+
render_serial_boot_offer, render_serial_ready,
132+
};
133+
134+
#[test]
135+
fn serializes_loader_control_messages() {
136+
let offer = SerialBootOfferMessage {
137+
protocol_version: SERIAL_PROTOCOL_VERSION,
138+
boot_id: "boot-1",
139+
kernel_url: "http://127.0.0.1/kernel.elf",
140+
kernel_size: 4096,
141+
image_format: ImageFormat::Elf64,
142+
arch: BootArch::X86_64,
143+
entry_symbol: Some("httpboot_entry"),
144+
};
145+
let value = serde_json::to_value(&offer).unwrap();
146+
assert_eq!(value["image_format"], "elf64");
147+
}
148+
149+
#[test]
150+
fn renders_and_parses_serial_ready_message() {
151+
let ready = SerialReadyMessage {
152+
protocol_version: SERIAL_PROTOCOL_VERSION,
153+
board: "asus-nuc15crh",
154+
arch: BootArch::X86_64,
155+
loader_version: Some("axloader"),
156+
};
157+
158+
let line = render_serial_ready(&ready).unwrap();
159+
160+
assert!(line.starts_with(SERIAL_READY_PREFIX));
161+
assert_eq!(parse_serial_ready(&line).unwrap(), ready);
162+
}
163+
164+
#[test]
165+
fn renders_and_parses_serial_boot_offer_message() {
166+
let offer = SerialBootOfferMessage {
167+
protocol_version: SERIAL_PROTOCOL_VERSION,
168+
boot_id: "boot-1",
169+
kernel_url: "http://10.3.10.192:2999/boot/kernel.elf",
170+
kernel_size: 4096,
171+
image_format: ImageFormat::Elf64,
172+
arch: BootArch::X86_64,
173+
entry_symbol: Some("httpboot_entry"),
174+
};
175+
176+
let line = render_serial_boot_offer(&offer).unwrap();
177+
178+
assert!(line.starts_with(SERIAL_BOOT_PREFIX));
179+
assert_eq!(parse_serial_boot_offer(&line).unwrap(), offer);
180+
}
181+
}

jkconfig/src/data/item.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -93,21 +93,21 @@ impl EnumItem {
9393
Err(SchemaError::TypeMismatch {
9494
path: path.to_string(),
9595
expected: format!("index 0-{}", self.variants.len() - 1),
96-
actual: format!("{}", idx),
96+
actual: format!("{idx}"),
9797
})
9898
}
9999
} else {
100100
Err(SchemaError::TypeMismatch {
101101
path: path.to_string(),
102102
expected: "non-negative integer".to_string(),
103-
actual: format!("{}", n),
103+
actual: format!("{n}"),
104104
})
105105
}
106106
}
107107
_ => Err(SchemaError::TypeMismatch {
108108
path: path.to_string(),
109109
expected: "string or number".to_string(),
110-
actual: format!("{}", value),
110+
actual: format!("{value}"),
111111
}),
112112
}
113113
}
@@ -128,7 +128,7 @@ impl ItemType {
128128
_ => Err(SchemaError::TypeMismatch {
129129
path: path.to_string(),
130130
expected: "string".to_string(),
131-
actual: format!("{}", value),
131+
actual: format!("{value}"),
132132
}),
133133
},
134134
ItemType::Number {
@@ -143,14 +143,14 @@ impl ItemType {
143143
Err(SchemaError::TypeMismatch {
144144
path: path.to_string(),
145145
expected: "number".to_string(),
146-
actual: format!("{}", n),
146+
actual: format!("{n}"),
147147
})
148148
}
149149
}
150150
_ => Err(SchemaError::TypeMismatch {
151151
path: path.to_string(),
152152
expected: "number".to_string(),
153-
actual: format!("{}", value),
153+
actual: format!("{value}"),
154154
}),
155155
},
156156
ItemType::Integer {
@@ -165,14 +165,14 @@ impl ItemType {
165165
Err(SchemaError::TypeMismatch {
166166
path: path.to_string(),
167167
expected: "integer".to_string(),
168-
actual: format!("{}", n),
168+
actual: format!("{n}"),
169169
})
170170
}
171171
}
172172
_ => Err(SchemaError::TypeMismatch {
173173
path: path.to_string(),
174174
expected: "integer".to_string(),
175-
actual: format!("{}", value),
175+
actual: format!("{value}"),
176176
}),
177177
},
178178
ItemType::Boolean {
@@ -186,7 +186,7 @@ impl ItemType {
186186
_ => Err(SchemaError::TypeMismatch {
187187
path: path.to_string(),
188188
expected: "boolean".to_string(),
189-
actual: format!("{}", value),
189+
actual: format!("{value}"),
190190
}),
191191
},
192192
ItemType::Enum(enum_item) => enum_item.update_from_value(value, path),
@@ -202,7 +202,7 @@ impl ItemType {
202202
return Err(SchemaError::TypeMismatch {
203203
path: path.to_string(),
204204
expected: "string, number, or boolean".to_string(),
205-
actual: format!("{}", item),
205+
actual: format!("{item}"),
206206
});
207207
}
208208
}
@@ -213,7 +213,7 @@ impl ItemType {
213213
_ => Err(SchemaError::TypeMismatch {
214214
path: path.to_string(),
215215
expected: "array".to_string(),
216-
actual: format!("{}", value),
216+
actual: format!("{value}"),
217217
}),
218218
},
219219
}

jkconfig/src/data/schema.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ impl WalkContext {
5454
self.get(field_name)
5555
.ok_or(SchemaError::SchemaConversionError {
5656
path: self.path.clone(),
57-
reason: format!("Missing required field '{}'", field_name),
57+
reason: format!("Missing required field '{field_name}'"),
5858
})
5959
}
6060

@@ -75,7 +75,7 @@ impl WalkContext {
7575
.map(|v| {
7676
v.as_str().ok_or(SchemaError::SchemaConversionError {
7777
path: self.path.clone(),
78-
reason: format!("Field '{}' is not a string", field_name),
78+
reason: format!("Field '{field_name}' is not a string"),
7979
})
8080
})
8181
.transpose()

jkconfig/src/web/server.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ pub async fn run_server(app_state: AppState, port: u16) -> anyhow::Result<()> {
1616
let addr = SocketAddr::from(([0, 0, 0, 0], port));
1717

1818
println!("🚀 Web服务器启动成功!");
19-
println!("📍 访问地址: http://localhost:{}", port);
19+
println!("📍 访问地址: http://localhost:{port}");
2020
println!("⏹️ 按 Ctrl+C 停止服务器");
2121

2222
// 启动服务器

0 commit comments

Comments
 (0)