diff --git a/Cargo.lock b/Cargo.lock index e2a5db3..5e0021c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -945,7 +945,7 @@ checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" [[package]] name = "fitimage" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "byteorder", @@ -1539,7 +1539,7 @@ dependencies = [ [[package]] name = "jkconfig" -version = "0.1.4" +version = "0.1.5" dependencies = [ "anyhow", "axum", @@ -1910,7 +1910,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "ostool" -version = "0.8.8" +version = "0.8.9" dependencies = [ "anyhow", "byte-unit", @@ -3167,7 +3167,7 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "uboot-shell" -version = "0.2.0" +version = "0.2.1" dependencies = [ "colored", "env_logger", diff --git a/fitimage/Cargo.toml b/fitimage/Cargo.toml index d518473..2ffbb96 100644 --- a/fitimage/Cargo.toml +++ b/fitimage/Cargo.toml @@ -1,10 +1,12 @@ [package] name = "fitimage" -version = "0.1.0" +version = "0.1.1" edition = "2021" authors = ["周睿 "] description = "A Rust library for creating U-Boot compatible FIT images" +documentation = "https://docs.rs/fitimage" license = "MIT OR Apache-2.0" +readme = "README.md" categories = ["embedded", "development-tools", "os"] keywords = ["u-boot", "boot", "fit-image", "embedded"] repository = "https://github.com/ZR233/ostool" diff --git a/fitimage/README.md b/fitimage/README.md index 46cd19f..2fea9b2 100644 --- a/fitimage/README.md +++ b/fitimage/README.md @@ -1,6 +1,6 @@ -# mkimage - FIT Image Library +# fitimage - FIT Image Library -一个用于创建U-Boot兼容FIT (Flattened Image Tree) 镜像的Rust库。 +一个用于创建 U-Boot 兼容 FIT (Flattened Image Tree) 镜像的 Rust 库。 ## 特性 @@ -18,13 +18,13 @@ ```toml [dependencies] -mkimage = "0.1.0" +fitimage = "0.1.0" ``` ### 基本用法 ```rust -use mkimage::{FitImageBuilder, FitImageConfig, ComponentConfig}; +use fitimage::{ComponentConfig, FitImageBuilder, FitImageConfig}; // 创建FIT镜像配置 let config = FitImageConfig::new("My FIT Image") @@ -32,12 +32,13 @@ let config = FitImageConfig::new("My FIT Image") ComponentConfig::new("kernel", kernel_data) .with_load_address(0x80080000) .with_entry_point(0x80080000) + .with_compression(true) ) .with_fdt( ComponentConfig::new("fdt", fdt_data) .with_load_address(0x82000000) ) - .with_kernel_compression(true); + ; // 构建FIT镜像 let mut builder = FitImageBuilder::new(); @@ -59,10 +60,13 @@ pub struct FitImageConfig { pub kernel: Option, pub fdt: Option, pub ramdisk: Option, - pub compress_kernel: bool, + pub default_config: Option, + pub configurations: std::collections::HashMap, } ``` +> `configurations` 用于生成多个启动配置;当未设置时会自动生成默认配置。 + ### ComponentConfig 单个组件的配置: @@ -84,20 +88,15 @@ pub struct ComponentConfig { impl FitImageBuilder { pub fn new() -> Self; pub fn build(&mut self, config: FitImageConfig) -> Result>; - pub fn build_with_compressor( - &mut self, - config: FitImageConfig, - compressor: Box - ) -> Result>; } ``` ## 示例 -### 完整FIT镜像 +### 完整 FIT 镜像 ```rust -use mkimage::{FitImageBuilder, FitImageConfig, ComponentConfig}; +use fitimage::{ComponentConfig, FitImageBuilder, FitImageConfig}; fn create_complete_fit() -> Result<(), Box> { let config = FitImageConfig::new("Complete FIT Image") @@ -105,6 +104,7 @@ fn create_complete_fit() -> Result<(), Box> { ComponentConfig::new("linux", kernel_data) .with_load_address(0x80080000) .with_entry_point(0x80080000) + .with_compression(true) ) .with_fdt( ComponentConfig::new("devicetree", fdt_data) @@ -114,7 +114,7 @@ fn create_complete_fit() -> Result<(), Box> { ComponentConfig::new("initramfs", ramdisk_data) .with_load_address(0x84000000) ) - .with_kernel_compression(true); + ; let mut builder = FitImageBuilder::new(); let fit_data = builder.build(config)?; @@ -131,33 +131,41 @@ fn create_complete_fit() -> Result<(), Box> { ```rust let config = FitImageConfig::new("Compressed FIT") - .with_kernel(kernel_component) - .with_kernel_compression(true); // 启用gzip压缩 + .with_kernel(kernel_component.with_compression(true)); // 启用gzip压缩 ``` ## 兼容性 -- ✅ U-Boot FIT格式兼容 +- ✅ U-Boot FIT 格式兼容 - ✅ 标准设备树结构 - ✅ ARM64架构支持 - ✅ Linux OS支持 +## TODO + +- [ ] 增加 bzip2 压缩支持 +- [ ] 增加 lzma 压缩支持 + ## 构建和测试 ```bash # 构建库 cargo build --lib -# 运行测试 -cargo test --lib +# 运行测试(含单元测试与集成测试) +cargo test -# 运行示例 -cargo run --example basic_usage -cargo run --example compression_test -cargo run --example full_fit_test +# 仅运行文档测试 +cargo test --doc ``` -## API文档 +## 测试建议 + +- 单元测试:覆盖哈希/CRC 计算、FDT 字符串表对齐、配置构建边界值。 +- 功能测试:使用 `mkimage`/`dumpimage` 对照验证结构与字段一致性。 +- 文档测试:为关键公开 API 添加可运行示例,保证 doctest 通过。 + +## API 文档 运行以下命令生成API文档: diff --git a/fitimage/src/compression/gzip.rs b/fitimage/src/compression/gzip.rs index be14707..1e911cd 100644 --- a/fitimage/src/compression/gzip.rs +++ b/fitimage/src/compression/gzip.rs @@ -1,6 +1,6 @@ -//! Gzip压缩实现 +//! Gzip compression implementation. //! -//! 使用flate2库提供gzip压缩和解压缩功能 +//! Provides gzip compression and decompression functionality using the flate2 library. use std::io::{Read, Write}; @@ -8,31 +8,34 @@ use crate::compression::traits::CompressionInterface; use crate::error::Result; use flate2::{read::GzDecoder, write::GzEncoder, Compression as GzipLevel}; -/// Gzip压缩器 -/// 支持可配置的压缩级别 +/// Gzip compressor with configurable compression level. pub struct GzipCompressor { - /// 压缩级别 (0-9, 0表示无压缩) + /// Compression level (0-9, where 0 means no compression). level: u8, - /// 是否启用压缩(false时直接复制数据) + /// Whether compression is enabled (false means data is copied directly). enabled: bool, } impl Default for GzipCompressor { fn default() -> Self { - Self::new(6) // + Self::new(6) } } impl GzipCompressor { - /// 创建指定压缩级别的gzip压缩器 + /// Creates a new gzip compressor with the specified compression level. + /// + /// # Arguments + /// + /// * `level` - Compression level from 0 to 9. Level 0 disables compression. pub fn new(level: u8) -> Self { Self { - level: level.clamp(0, 9), // 限制在0-9范围内 - enabled: level > 0, // 级别0表示不压缩 + level: level.clamp(0, 9), + enabled: level > 0, } } - /// 创建禁用压缩的实例 + /// Creates a disabled compressor instance that passes data through unchanged. pub fn new_disabled() -> Self { Self { level: 0, @@ -40,7 +43,7 @@ impl GzipCompressor { } } - /// 获取flate2的压缩级别 + /// Gets the flate2 compression level. fn get_compression_level(&self) -> GzipLevel { if !self.enabled { GzipLevel::none() @@ -58,7 +61,7 @@ impl GzipCompressor { impl CompressionInterface for GzipCompressor { fn compress(&self, data: &[u8]) -> Result> { if !self.enabled { - // 如果禁用压缩,直接返回数据副本 + // If compression is disabled, return a copy of the data. return Ok(data.to_vec()); } @@ -75,7 +78,7 @@ impl CompressionInterface for GzipCompressor { fn decompress(&self, compressed_data: &[u8]) -> Result> { if !self.enabled { - // 如果没有压缩,直接返回数据副本 + // If compression was not applied, return a copy of the data. return Ok(compressed_data.to_vec()); } diff --git a/fitimage/src/compression/mod.rs b/fitimage/src/compression/mod.rs index 824c06a..4995989 100644 --- a/fitimage/src/compression/mod.rs +++ b/fitimage/src/compression/mod.rs @@ -1,6 +1,6 @@ -//! 压缩功能模块 +//! Compression module. //! -//! 提供各种压缩算法的统一接口,支持gzip、bzip2、lzma等格式 +//! Provides unified interface for compression algorithms. Currently supports gzip. pub mod gzip; pub mod traits; diff --git a/fitimage/src/compression/traits.rs b/fitimage/src/compression/traits.rs index 1567792..7bd8879 100644 --- a/fitimage/src/compression/traits.rs +++ b/fitimage/src/compression/traits.rs @@ -1,30 +1,35 @@ -//! 压缩接口定义 +//! Compression interface definitions. //! -//! 定义了所有压缩算法需要实现的标准接口 +//! Defines the standard interface that all compression algorithms must implement. use crate::error::Result; -/// 压缩接口trait -/// 所有压缩算法都需要实现这个接口 +/// Compression interface trait. +/// +/// All compression algorithms must implement this interface. pub trait CompressionInterface { - /// 压缩数据 + /// Compresses data. /// - /// # 参数 - /// - `data`: 要压缩的原始数据 + /// # Arguments /// - /// # 返回 - /// 压缩后的数据 + /// * `data` - The raw data to compress + /// + /// # Returns + /// + /// The compressed data. fn compress(&self, data: &[u8]) -> Result>; - /// 解压缩数据(主要用于验证) + /// Decompresses data (mainly used for verification). + /// + /// # Arguments + /// + /// * `compressed_data` - The compressed data /// - /// # 参数 - /// - `compressed_data`: 已压缩的数据 + /// # Returns /// - /// # 返回 - /// 解压缩后的原始数据 + /// The decompressed original data. fn decompress(&self, compressed_data: &[u8]) -> Result>; - /// 获取压缩算法名称 + /// Returns the name of the compression algorithm. fn get_name(&self) -> &'static str; } diff --git a/fitimage/src/error.rs b/fitimage/src/error.rs index c471561..bf66141 100644 --- a/fitimage/src/error.rs +++ b/fitimage/src/error.rs @@ -1,11 +1,11 @@ -//! Error types for mkimage operations +//! Error types for fitimage operations use thiserror::Error; -/// Result type alias for mkimage operations +/// Result type alias for fitimage operations pub type Result = std::result::Result; -/// Errors that can occur during mkimage operations +/// Errors that can occur during fitimage operations #[derive(Error, Debug)] pub enum MkImageError { #[error("Invalid image data: {0}")] diff --git a/fitimage/src/fit/config.rs b/fitimage/src/fit/config.rs index 7d45ef1..66d0699 100644 --- a/fitimage/src/fit/config.rs +++ b/fitimage/src/fit/config.rs @@ -4,12 +4,15 @@ use serde::{Deserialize, Serialize}; +/// Supported compression algorithms for FIT components. #[derive(Debug, Clone, Serialize, Deserialize, Copy, PartialEq, Eq)] pub enum CompressionAlgorithm { + /// Gzip compression. Gzip, } impl CompressionAlgorithm { + /// Return the string name used in FIT properties. pub fn as_str(&self) -> &'static str { match self { CompressionAlgorithm::Gzip => "gzip", @@ -17,7 +20,7 @@ impl CompressionAlgorithm { } } -/// Configuration for building a FIT image +/// Configuration for building a FIT image. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FitImageConfig { /// Description of the FIT image @@ -35,16 +38,22 @@ pub struct FitImageConfig { /// Default configuration name pub default_config: Option, - /// Configurations mapping (name -> (description, kernel, fdt, ramdisk)) + /// Configurations mapping (name -> description, kernel, fdt, ramdisk). pub configurations: std::collections::HashMap, } +/// A named configuration that references image nodes. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FitConfiguration { + /// Configuration node name. pub name: String, + /// Human-readable configuration description. pub description: String, + /// Kernel image node reference. pub kernel: Option, + /// FDT image node reference. pub fdt: Option, + /// Ramdisk image node reference. pub ramdisk: Option, } @@ -69,6 +78,7 @@ pub struct ComponentConfig { /// OS type (linux, etc.) pub os: Option, + /// Whether to gzip-compress this component before embedding. pub compression: bool, /// Load address in memory @@ -118,7 +128,7 @@ impl ComponentConfig { self } - /// Set compression type + /// Enable or disable gzip compression for this component. pub fn with_compression(mut self, b: bool) -> Self { self.compression = b; self @@ -138,7 +148,7 @@ impl ComponentConfig { } impl FitImageConfig { - /// Create a new FIT image configuration + /// Create a new FIT image configuration with a top-level description. pub fn new(description: impl Into) -> Self { Self { description: description.into(), @@ -150,31 +160,31 @@ impl FitImageConfig { } } - /// Set kernel component + /// Set kernel component. pub fn with_kernel(mut self, kernel: ComponentConfig) -> Self { self.kernel = Some(kernel); self } - /// Set FDT component + /// Set FDT component. pub fn with_fdt(mut self, fdt: ComponentConfig) -> Self { self.fdt = Some(fdt); self } - /// Set ramdisk component + /// Set ramdisk component. pub fn with_ramdisk(mut self, ramdisk: ComponentConfig) -> Self { self.ramdisk = Some(ramdisk); self } - /// Set default configuration + /// Set default configuration name. pub fn with_default_config(mut self, default: impl Into) -> Self { self.default_config = Some(default.into()); self } - /// Add a configuration + /// Add a configuration entry that references image node names. pub fn with_configuration( mut self, name: impl Into, diff --git a/fitimage/src/fit/fdt_header.rs b/fitimage/src/fit/fdt_header.rs index ecfa17c..7fc25e6 100644 --- a/fitimage/src/fit/fdt_header.rs +++ b/fitimage/src/fit/fdt_header.rs @@ -256,7 +256,7 @@ mod tests { assert_eq!(MemReserveEntry::size(), 16); // 2 * 8 bytes } - #[test] + // #[test] // fn test_mem_reserve_entry_write() { // let entry = MemReserveEntry::new(0x12345678, 0xABCDEF00); // let mut buffer = Vec::new(); diff --git a/fitimage/src/fit/mod.rs b/fitimage/src/fit/mod.rs index 9e68ac7..8a49916 100644 --- a/fitimage/src/fit/mod.rs +++ b/fitimage/src/fit/mod.rs @@ -1,6 +1,6 @@ -//! FIT (Flattened Image Tree) 模块 +//! FIT (Flattened Image Tree) module. //! -//! 实现U-Boot FIT image格式的创建和处理功能 +//! Provides functionality for creating and processing U-Boot FIT image format. pub mod builder; pub mod config; diff --git a/fitimage/src/lib.rs b/fitimage/src/lib.rs index b5bca9b..d5d9cf6 100644 --- a/fitimage/src/lib.rs +++ b/fitimage/src/lib.rs @@ -1,7 +1,58 @@ +//! # fitimage - FIT Image Library +//! +//! A Rust library for creating U-Boot compatible FIT (Flattened Image Tree) images. +//! +//! ## Features +//! +//! - Complete FIT image creation functionality +//! - Support for kernel, FDT (device tree), and ramdisk components +//! - Gzip compression support +//! - Multiple hash algorithms (MD5, SHA1, CRC32) +//! - U-Boot compatible device tree structure +//! +//! ## Quick Start +//! +//! ```rust,no_run +//! use fitimage::{FitImageBuilder, FitImageConfig, ComponentConfig}; +//! +//! // Create FIT image configuration +//! let config = FitImageConfig::new("My FIT Image") +//! .with_kernel( +//! ComponentConfig::new("kernel", vec![/* kernel data */]) +//! .with_load_address(0x80080000) +//! .with_entry_point(0x80080000) +//! ) +//! .with_fdt( +//! ComponentConfig::new("fdt", vec![/* fdt data */]) +//! .with_load_address(0x82000000) +//! ); +//! +//! // Build FIT image +//! let mut builder = FitImageBuilder::new(); +//! let fit_data = builder.build(config).unwrap(); +//! ``` +//! +//! ## Modules +//! +//! - [`fit`] - Core FIT image building functionality +//! - [`compression`] - Compression algorithms (gzip) +//! - [`hash`] - Hash calculation utilities (MD5, SHA1, CRC32) +//! - [`crc`] - CRC32 checksum calculation +//! - [`error`] - Error types and result definitions + +/// Compression algorithms support (gzip, etc.) pub mod compression; + +/// CRC32 checksum calculation utilities. pub mod crc; + +/// Error types and result definitions for FIT image operations. pub mod error; + +/// Core FIT image building functionality. pub mod fit; + +/// Hash calculation utilities (MD5, SHA1, CRC32). pub mod hash; // Re-export main types for convenience @@ -11,7 +62,7 @@ pub use error::{MkImageError, Result}; pub use fit::{ComponentConfig, FitImageBuilder, FitImageConfig}; pub use hash::{calculate_hashes, default_hash_algorithms, HashAlgorithm, HashResult}; -/// Current version of the mkimage implementation +/// Current version of the fitimage implementation pub const VERSION: &str = env!("CARGO_PKG_VERSION"); /// FIT image magic number diff --git a/jkconfig/Cargo.toml b/jkconfig/Cargo.toml index c830ac6..6ec7926 100644 --- a/jkconfig/Cargo.toml +++ b/jkconfig/Cargo.toml @@ -2,12 +2,14 @@ authors = ["周睿 "] categories = ["command-line-interface", "config"] description = "A Cursive-based TUI component library for JSON Schema configuration" +documentation = "https://docs.rs/jkconfig" edition = "2024" keywords = ["tui", "config", "json-schema", "terminal"] license = "MIT OR Apache-2.0" name = "jkconfig" +readme = "README.md" repository = "https://github.com/ZR233/ostool" -version = "0.1.4" +version = "0.1.5" [[bin]] name = "jkconfig" diff --git a/jkconfig/src/data/app_data.rs b/jkconfig/src/data/app_data.rs index 2856c70..f985231 100644 --- a/jkconfig/src/data/app_data.rs +++ b/jkconfig/src/data/app_data.rs @@ -11,29 +11,43 @@ use cursive::Cursive; use crate::data::{menu::MenuRoot, types::ElementType}; +/// Callback used to provide the list of available features. pub type FeaturesCallback = Arc Vec + Send + Sync>; +/// Callback invoked when a menu element is entered. pub type HockCallback = Arc; +/// Hook registration for a specific menu path. #[derive(Clone)] pub struct ElemHock { + /// Menu path string (dot-separated). pub path: String, + /// Callback executed when entering the path. pub callback: HockCallback, } +/// Application state container for schema-driven config editing. #[derive(Clone)] pub struct AppData { + /// Root menu parsed from JSON Schema. pub root: MenuRoot, + /// Current menu path as a list of keys. pub current_key: Vec, + /// Whether configuration has pending changes. pub needs_save: bool, + /// Path to the configuration file. pub config: PathBuf, + /// Custom user data storage. pub user_data: HashMap, + /// Temporary data used by editors. pub temp_data: Option<(String, serde_json::Value)>, + /// Registered element hooks. pub elem_hocks: Vec, } const DEFAULT_CONFIG_PATH: &str = ".config.toml"; +/// Derive a default schema path from a config path. pub fn default_schema_by_init(config: &Path) -> PathBuf { let binding = config.file_name().unwrap().to_string_lossy(); let mut name_split = binding.split(".").collect::>(); @@ -51,6 +65,9 @@ pub fn default_schema_by_init(config: &Path) -> PathBuf { } impl AppData { + /// Build `AppData` from optional config and schema paths. + /// + /// When schema is not provided, it is auto-derived from the config path. pub fn new( config: Option>, schema: Option>, @@ -80,6 +97,9 @@ impl AppData { init_value_path } + /// Build `AppData` from an initial content string and a schema value. + /// + /// This is useful when config content has already been loaded. pub fn new_with_init_and_schema( init: &str, init_value_path: &Path, @@ -116,6 +136,9 @@ impl AppData { }) } + /// Build `AppData` from a schema value and an optional config path. + /// + /// If the config file exists, it is loaded to initialize values. pub fn new_with_schema( config: Option>, schema: &serde_json::Value, @@ -156,6 +179,7 @@ impl AppData { }) } + /// Persist changes and create a timestamped backup when needed. pub fn on_exit(&mut self) -> anyhow::Result<()> { if !self.needs_save { return Ok(()); @@ -193,6 +217,7 @@ impl AppData { Ok(()) } + /// Enter a submenu path (dot-separated). pub fn enter(&mut self, key: &str) { if key.is_empty() { return; @@ -200,17 +225,19 @@ impl AppData { self.current_key = key.split(".").map(|s| s.to_string()).collect(); } + /// Push a field name onto the current path. pub fn push_field(&mut self, f: &str) { self.current_key.push(f.to_string()); } - /// 返回上级路径 + /// Navigate back to the parent path. pub fn navigate_back(&mut self) { if !self.current_key.is_empty() { self.current_key.pop(); } } + /// Return the current path as a dot-separated string. pub fn key_string(&self) -> String { if self.current_key.is_empty() { return String::new(); @@ -219,10 +246,12 @@ impl AppData { self.current_key.join(".") } + /// Get the element at the current path. pub fn current(&self) -> Option<&ElementType> { self.root.get_by_key(&self.key_string()) } + /// Get the mutable element at the current path. pub fn current_mut(&mut self) -> Option<&mut ElementType> { self.root.get_mut_by_key(&self.key_string()) } diff --git a/jkconfig/src/data/item.rs b/jkconfig/src/data/item.rs index c4d837c..d1dac61 100644 --- a/jkconfig/src/data/item.rs +++ b/jkconfig/src/data/item.rs @@ -2,34 +2,42 @@ use crate::data::{schema::SchemaError, types::ElementBase}; use serde_json::Value; +/// Leaf configuration item with a concrete value type. #[derive(Debug, Clone)] pub struct Item { + /// Shared element metadata. pub base: ElementBase, + /// Value storage and type information. pub item_type: ItemType, } +/// Supported value types for leaf items. #[derive(Debug, Clone)] pub enum ItemType { + /// String value with optional default. String { value: Option, default: Option, }, + /// Floating-point number value with optional default. Number { value: Option, default: Option, }, + /// Integer value with optional default. Integer { value: Option, default: Option, }, - Boolean { - value: bool, - default: bool, - }, + /// Boolean value with default. + Boolean { value: bool, default: bool }, + /// Enum selection by index. Enum(EnumItem), + /// Array of scalar values stored as strings. Array(ArrayItem), } +/// Array item metadata and values. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ArrayItem { /// Array element type (e.g., "string", "integer") @@ -40,19 +48,27 @@ pub struct ArrayItem { pub default: Vec, } +/// Enum variants and selected index. #[derive(Debug, Clone)] pub struct EnumItem { + /// List of variant labels. pub variants: Vec, + /// Selected variant index. pub value: Option, + /// Default variant index. pub default: Option, } impl EnumItem { + /// Get the currently selected variant as string, if any. pub fn value_str(&self) -> Option<&str> { self.value .and_then(|idx| self.variants.get(idx).map(String::as_str)) } + /// Update enum selection from JSON value. + /// + /// Accepts string values matching a variant or numeric indices. pub fn update_from_value(&mut self, value: &Value, path: &str) -> Result<(), SchemaError> { match value { Value::String(s) => { @@ -98,6 +114,7 @@ impl EnumItem { } impl ItemType { + /// Update the stored value from JSON. pub fn update_from_value(&mut self, value: &Value, path: &str) -> Result<(), SchemaError> { match self { ItemType::String { @@ -204,6 +221,7 @@ impl ItemType { } impl Item { + /// Serialize the item into a JSON value. pub fn as_json(&self) -> Value { match &self.item_type { ItemType::String { value, .. } => match value { @@ -250,6 +268,7 @@ impl Item { } } + /// Update the item from a JSON value. pub fn update_from_value(&mut self, value: &Value) -> Result<(), SchemaError> { let path = self.base.key(); self.item_type.update_from_value(value, &path) diff --git a/jkconfig/src/data/menu.rs b/jkconfig/src/data/menu.rs index 907b821..07e523e 100644 --- a/jkconfig/src/data/menu.rs +++ b/jkconfig/src/data/menu.rs @@ -11,14 +11,19 @@ use crate::data::{ types::{ElementBase, ElementType}, }; +/// Root container for schema-derived menu tree. #[derive(Clone)] pub struct MenuRoot { + /// JSON Schema version string. pub schema_version: String, + /// Root title displayed in the UI. pub title: String, + /// Root element (must be a menu). pub menu: ElementType, } impl MenuRoot { + /// Get the root menu node (panics if root is not a menu). pub fn menu(&self) -> &Menu { match &self.menu { ElementType::Menu(menu) => menu, @@ -26,6 +31,7 @@ impl MenuRoot { } } + /// Get the mutable root menu node (panics if root is not a menu). pub fn menu_mut(&mut self) -> &mut Menu { match &mut self.menu { ElementType::Menu(menu) => menu, @@ -33,6 +39,7 @@ impl MenuRoot { } } + /// Get an element by its dot-separated key. pub fn get_by_key(&self, key: &str) -> Option<&ElementType> { if key.is_empty() { return Some(&self.menu); @@ -42,6 +49,7 @@ impl MenuRoot { self.menu().get_by_field_path(&ks) } + /// Get a mutable element by its dot-separated key. pub fn get_mut_by_key(&mut self, key: &str) -> Option<&mut ElementType> { if key.is_empty() { return Some(&mut self.menu); @@ -50,10 +58,12 @@ impl MenuRoot { self.menu_mut().get_mut_by_field_path(&ks) } + /// Update the menu tree from a JSON object value. pub fn update_by_value(&mut self, value: &Value) -> Result<(), SchemaError> { self.menu.update_from_value(value, None) } + /// Serialize the menu tree into a JSON value. pub fn as_json(&self) -> Value { self.menu().as_json() } @@ -74,15 +84,19 @@ impl Debug for MenuRoot { } } -/// Menu => type: object +/// Menu node for schema objects (type: object). #[derive(Clone)] pub struct Menu { + /// Shared element metadata. pub base: ElementBase, + /// Child elements for each field. pub children: Vec, + /// Whether this menu has been set by user input. pub is_set: bool, } impl Menu { + /// Serialize this menu into a JSON object. pub fn as_json(&self) -> Value { let mut result = serde_json::Map::new(); @@ -124,6 +138,7 @@ impl Menu { Value::Object(result) } + /// Get a child element by its path segments. pub fn get_by_field_path(&self, field_path: &[&str]) -> Option<&ElementType> { if field_path.is_empty() { return None; @@ -144,6 +159,7 @@ impl Menu { } } + /// Get a mutable child element by its path segments. pub fn get_mut_by_field_path(&mut self, field_path: &[&str]) -> Option<&mut ElementType> { if field_path.is_empty() { return None; @@ -164,6 +180,7 @@ impl Menu { } } + /// Update this menu from a JSON object value. pub fn update_from_value(&mut self, value: &Value) -> Result<(), SchemaError> { let value = value.as_object().ok_or(SchemaError::TypeMismatch { path: self.key(), @@ -183,6 +200,7 @@ impl Menu { Ok(()) } + /// Whether this menu is considered unset. pub fn is_none(&self) -> bool { if self.is_required { return false; @@ -190,10 +208,12 @@ impl Menu { !self.is_set } + /// Return a copy of child elements for UI rendering. pub fn fields(&self) -> Vec { self.children.to_vec() } + /// Get a direct child by field name. pub fn get_child_by_key(&self, key: &str) -> Option<&ElementType> { self.children .iter() @@ -201,6 +221,7 @@ impl Menu { .map(|v| v as _) } + /// Get a mutable direct child by field name. pub fn get_child_mut_by_key(&mut self, key: &str) -> Option<&mut ElementType> { self.children .iter_mut() diff --git a/jkconfig/src/data/mod.rs b/jkconfig/src/data/mod.rs index a749872..8576940 100644 --- a/jkconfig/src/data/mod.rs +++ b/jkconfig/src/data/mod.rs @@ -1,8 +1,39 @@ +//! Configuration data structures and schema parsing. +//! +//! This module provides the core data structures for managing JSON Schema-based +//! configuration, including: +//! +//! - Schema parsing and conversion to internal representation +//! - Configuration value management +//! - Serialization to TOML/JSON formats +//! +//! ## Architecture +//! +//! The data module is organized into several submodules: +//! +//! - [`app_data`] - Main application data container +//! - [`item`] - Individual configuration items +//! - [`menu`] - Menu structure for navigation +//! - [`oneof`] - OneOf/AnyOf schema variant handling +//! - [`schema`] - JSON Schema parsing utilities +//! - [`types`] - Element type definitions + +/// Main application data container and configuration management. pub mod app_data; + +/// Individual configuration item representation. pub mod item; + +/// Menu structure for hierarchical navigation. pub mod menu; + +/// OneOf/AnyOf schema variant handling. pub mod oneof; + +/// JSON Schema parsing utilities. pub mod schema; + +/// Element type definitions for different data types. pub mod types; pub use app_data::AppData; diff --git a/jkconfig/src/data/oneof.rs b/jkconfig/src/data/oneof.rs index 60489eb..b8e2ad8 100644 --- a/jkconfig/src/data/oneof.rs +++ b/jkconfig/src/data/oneof.rs @@ -12,24 +12,32 @@ use crate::data::{ use log::trace; use serde_json::Value; +/// OneOf/AnyOf variant container. #[derive(Clone)] pub struct OneOf { + /// Shared element metadata. pub base: ElementBase, + /// Available variant elements. pub variants: Vec, + /// Selected variant index. pub selected_index: Option, + /// Default variant index. pub default_index: Option, } impl OneOf { + /// Get the currently selected variant. pub fn selected(&self) -> Option<&ElementType> { self.selected_index.and_then(|idx| self.variants.get(idx)) } + /// Get the currently selected variant mutably. pub fn selected_mut(&mut self) -> Option<&mut ElementType> { self.selected_index .and_then(move |idx| self.variants.get_mut(idx)) } + /// Resolve a display name for a variant index. pub fn variant_display(&self, idx: usize) -> String { if let Some(variant) = self.variants.get(idx) { match variant { @@ -49,6 +57,7 @@ impl OneOf { } } + /// Get an element by path segments from the selected variant. pub fn get_by_field_path(&self, field_path: &[&str]) -> Option<&ElementType> { if field_path.is_empty() { return None; @@ -78,6 +87,7 @@ impl OneOf { None } + /// Get a mutable element by path segments from the selected variant. pub fn get_mut_by_field_path(&mut self, field_path: &[&str]) -> Option<&mut ElementType> { if field_path.is_empty() { return None; @@ -116,6 +126,7 @@ impl OneOf { variant.update_from_value(value, name).is_ok() } + /// Update selection and data from a JSON value. pub fn update_from_value(&mut self, value: &Value) -> Result<(), SchemaError> { let mut name: Option = None; let mut value = value; @@ -149,6 +160,7 @@ impl OneOf { }) } + /// Serialize the selected variant into JSON. pub fn as_json(&self) -> Value { if let Some(selected) = self.selected() { match selected { @@ -174,10 +186,12 @@ impl OneOf { } } + /// Get the field name for this OneOf element. pub fn field_name(&self) -> String { self.base.field_name() } + /// Select a variant by index and initialize defaults when needed. pub fn set_selected_index(&mut self, index: usize) -> Result<(), SchemaError> { let path = self.path.clone(); let v = self @@ -201,6 +215,7 @@ impl OneOf { Ok(()) } + /// Whether no variant is selected. pub fn is_none(&self) -> bool { self.selected_index.is_none() } diff --git a/jkconfig/src/data/schema.rs b/jkconfig/src/data/schema.rs index 8fd6b1e..3129b67 100644 --- a/jkconfig/src/data/schema.rs +++ b/jkconfig/src/data/schema.rs @@ -9,6 +9,7 @@ use crate::data::{ types::{ElementBase, ElementType}, }; +/// Errors produced while converting JSON Schema into internal structures. #[derive(thiserror::Error, Debug)] pub enum SchemaError { #[error("Unsupported schema")] diff --git a/jkconfig/src/data/types.rs b/jkconfig/src/data/types.rs index 483fe9f..55d542e 100644 --- a/jkconfig/src/data/types.rs +++ b/jkconfig/src/data/types.rs @@ -7,16 +7,23 @@ use crate::data::{item::Item, menu::Menu, oneof::OneOf, schema::SchemaError}; use serde_json::Value; +/// Common fields shared by all schema elements. #[derive(Debug, Clone, Default)] pub struct ElementBase { + /// Schema path for this element. pub path: PathBuf, + /// Display title derived from schema description or field name. pub title: String, + /// Help text from schema description. pub help: Option, + /// Whether this field is required by the schema. pub is_required: bool, + /// Struct or variant name used for display. pub struct_name: String, } impl ElementBase { + /// Create a new base element from schema metadata. pub fn new( path: &Path, description: Option, @@ -43,6 +50,7 @@ impl ElementBase { } } + /// Return the dot-separated key for this element. pub fn key(&self) -> String { self.path .iter() @@ -51,6 +59,7 @@ impl ElementBase { .join(".") } + /// Return the last path segment as the field name. pub fn field_name(&self) -> String { self.path .iter() @@ -60,6 +69,7 @@ impl ElementBase { } } +/// High-level element types used by the UI and serialization logic. #[derive(Debug, Clone)] pub enum ElementType { Menu(Menu), @@ -90,6 +100,7 @@ impl DerefMut for ElementType { } impl ElementType { + /// Update the element value from a JSON value. pub fn update_from_value( &mut self, value: &Value, @@ -113,6 +124,7 @@ impl ElementType { } } + /// Whether this element is considered unset. pub fn is_none(&self) -> bool { match self { ElementType::Menu(menu) => menu.is_none(), @@ -128,6 +140,7 @@ impl ElementType { } } + /// Reset this element to an "unset" state when allowed. pub fn set_none(&mut self) { if self.is_required { return; diff --git a/jkconfig/src/lib.rs b/jkconfig/src/lib.rs index 3aba6aa..381a5b4 100644 --- a/jkconfig/src/lib.rs +++ b/jkconfig/src/lib.rs @@ -1,15 +1,65 @@ +//! # jkconfig +//! +//! A Cursive-based TUI component library for JSON Schema configuration. +//! +//! JKConfig automatically generates interactive terminal forms from JSON Schema +//! definitions, making configuration management intuitive and error-free. +//! +//! ## Features +//! +//! - Beautiful TUI interface built with [Cursive](https://github.com/gyscos/cursive) +//! - JSON Schema driven UI generation (Draft 2020-12) +//! - Support for multiple data types: String, Integer, Number, Boolean, Enum, Array, Object, OneOf +//! - Multi-format support: TOML and JSON configuration files +//! - Keyboard shortcuts with Vim-like keybindings +//! - Real-time type validation based on schema constraints +//! - Automatic backup before saving changes +//! +//! ## Quick Start +//! +//! ```rust,no_run +//! use jkconfig::data::AppData; +//! +//! // Load configuration with schema +//! let app_data = AppData::new( +//! Some("config.toml"), +//! Some("config-schema.json") +//! ).unwrap(); +//! +//! // Access the configuration tree +//! let json_value = app_data.root.as_json(); +//! ``` +//! +//! ## Modules +//! +//! - [`data`] - Configuration data structures and schema parsing +//! - [`run`] - TUI application runner +//! - [`ui`] - UI components and editors +//! - [`web`] - Web server module (requires `web` feature) + // #[macro_use] // extern crate log; #[macro_use] mod log; +/// Configuration data structures and schema parsing. +/// +/// This module provides the core data structures for managing configuration +/// data, including schema parsing, value management, and serialization. pub mod data; + // UI模块暂时注释掉,使用主程序中的 MenuView +/// TUI application runner and main entry points. pub mod run; + +/// UI components and editors for different data types. pub mod ui; // Web服务器模块(需要web feature) +/// Web server module for remote configuration editing. +/// +/// This module is only available when the `web` feature is enabled. #[cfg(feature = "web")] pub mod web; diff --git a/jkconfig/src/run.rs b/jkconfig/src/run.rs index 6dd054f..f0c8bce 100644 --- a/jkconfig/src/run.rs +++ b/jkconfig/src/run.rs @@ -13,6 +13,14 @@ use crate::{ pub use crate::data::app_data::ElemHock; +/// Run the configuration editor workflow for a typed config. +/// +/// When `always_use_ui` is false and the config file can be parsed, +/// the parsed config is returned without launching the UI. +/// +/// # Errors +/// +/// Returns errors when schema generation, parsing, or I/O fails. pub async fn run( config_path: impl AsRef, always_use_ui: bool, diff --git a/ostool/Cargo.toml b/ostool/Cargo.toml index b53c322..461d3d3 100644 --- a/ostool/Cargo.toml +++ b/ostool/Cargo.toml @@ -1,13 +1,15 @@ [package] authors = ["周睿 "] -categories = ["os", "embedded", "development-tools", "config"] +categories = ["command-line-utilities", "embedded", "development-tools"] description = "A tool for operating system development" +documentation = "https://docs.rs/ostool" edition = "2024" +keywords = ["os", "embedded", "qemu", "u-boot", "bootloader"] license = "MIT OR Apache-2.0" name = "ostool" readme = "../README.md" repository = "https://github.com/ZR233/ostool" -version = "0.8.8" +version = "0.8.9" [[bin]] name = "ostool" diff --git a/ostool/src/build/cargo_builder.rs b/ostool/src/build/cargo_builder.rs index 1fa90ac..c57bd6a 100644 --- a/ostool/src/build/cargo_builder.rs +++ b/ostool/src/build/cargo_builder.rs @@ -1,3 +1,9 @@ +//! Cargo build command builder and executor. +//! +//! This module provides the [`CargoBuilder`] type for constructing and executing +//! Cargo build commands with customizable options, environment variables, and +//! pre/post build hooks. + use std::{ collections::HashMap, path::{Path, PathBuf}, @@ -7,6 +13,21 @@ use colored::Colorize; use crate::{build::config::Cargo, ctx::AppContext, utils::Command}; +/// A builder for constructing and executing Cargo commands. +/// +/// `CargoBuilder` provides a fluent API for configuring Cargo build or run +/// commands with custom arguments, environment variables, and build hooks. +/// +/// # Example +/// +/// ```rust,no_run +/// use ostool::build::cargo_builder::CargoBuilder; +/// use ostool::build::config::Cargo; +/// use ostool::ctx::AppContext; +/// +/// // CargoBuilder is typically used internally by AppContext +/// // See AppContext::cargo_build() and AppContext::cargo_run() +/// ``` pub struct CargoBuilder<'a> { ctx: &'a mut AppContext, config: &'a Cargo, @@ -18,6 +39,13 @@ pub struct CargoBuilder<'a> { } impl<'a> CargoBuilder<'a> { + /// Creates a new `CargoBuilder` for executing `cargo build`. + /// + /// # Arguments + /// + /// * `ctx` - The application context. + /// * `config` - The Cargo build configuration. + /// * `config_path` - Optional path to the configuration file. pub fn build(ctx: &'a mut AppContext, config: &'a Cargo, config_path: Option) -> Self { Self { ctx, @@ -30,6 +58,13 @@ impl<'a> CargoBuilder<'a> { } } + /// Creates a new `CargoBuilder` for executing `cargo run`. + /// + /// # Arguments + /// + /// * `ctx` - The application context. + /// * `config` - The Cargo build configuration. + /// * `config_path` - Optional path to the configuration file. pub fn run(ctx: &'a mut AppContext, config: &'a Cargo, config_path: Option) -> Self { Self { ctx, @@ -42,30 +77,38 @@ impl<'a> CargoBuilder<'a> { } } + /// Returns `true` if this builder is configured for `cargo run`. pub fn is_run(&self) -> bool { self.command == "run" } + /// Sets the debug mode for the build. + /// + /// When enabled, builds in debug mode and enables GDB server for QEMU. pub fn debug(self, debug: bool) -> Self { self.ctx.debug = debug; self } + /// Creates a build command using the context's stored config path. pub fn build_auto(ctx: &'a mut AppContext, config: &'a Cargo) -> Self { let config_path = ctx.build_config_path.clone(); Self::build(ctx, config, config_path) } + /// Creates a run command using the context's stored config path. pub fn run_auto(ctx: &'a mut AppContext, config: &'a Cargo) -> Self { let config_path = ctx.build_config_path.clone(); Self::run(ctx, config, config_path) } + /// Adds a single argument to the Cargo command. pub fn arg(mut self, arg: impl Into) -> Self { self.extra_args.push(arg.into()); self } + /// Adds multiple arguments to the Cargo command. pub fn args(mut self, args: I) -> Self where I: IntoIterator, @@ -75,16 +118,26 @@ impl<'a> CargoBuilder<'a> { self } + /// Sets an environment variable for the Cargo command. pub fn env(mut self, key: impl Into, value: impl Into) -> Self { self.extra_envs.insert(key.into(), value.into()); self } + /// Sets whether to skip the objcopy step after building. pub fn skip_objcopy(mut self, skip: bool) -> Self { self.skip_objcopy = skip; self } + /// Executes the configured Cargo command. + /// + /// This runs pre-build commands, executes Cargo, handles output artifacts, + /// and runs post-build commands. + /// + /// # Errors + /// + /// Returns an error if any step of the build process fails. pub async fn execute(mut self) -> anyhow::Result<()> { // 1. Pre-build commands self.run_pre_build_cmds()?; diff --git a/ostool/src/build/config.rs b/ostool/src/build/config.rs index 72a0978..ef4e273 100644 --- a/ostool/src/build/config.rs +++ b/ostool/src/build/config.rs @@ -1,66 +1,113 @@ +//! Build configuration types and structures. +//! +//! This module defines the configuration structures used to specify how +//! operating system projects should be built. Configuration is typically +//! stored in `.build.toml` files. +//! +//! # Configuration File Format +//! +//! ```toml +//! [system.Cargo] +//! target = "aarch64-unknown-none" +//! package = "my-kernel" +//! features = ["feature1", "feature2"] +//! to_bin = true +//! ``` + use std::collections::HashMap; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +/// Root build configuration structure. +/// +/// This is the top-level configuration that specifies which build system +/// to use (Cargo or custom shell commands). #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct BuildConfig { + /// The build system configuration. pub system: BuildSystem, } +/// Specifies the build system to use. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] pub enum BuildSystem { + /// Use custom shell commands for building. Custom(Custom), + /// Use Cargo for building. Cargo(Cargo), } +/// Configuration for custom (non-Cargo) build systems. +/// +/// This allows using arbitrary shell commands for building, +/// useful for projects that don't use Cargo or need special build steps. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct Custom { - /// shell command to build the kernel + /// Shell command to build the kernel. pub build_cmd: String, - /// path to the built ELF file + /// Path to the built ELF file. pub elf_path: String, - /// whether to output as binary + /// Whether to convert the ELF to raw binary format. pub to_bin: bool, } +/// Configuration for Cargo-based builds. +/// +/// This structure contains all the options needed to configure a Cargo build, +/// including target architecture, features, environment variables, and build hooks. #[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct Cargo { - /// environment variables + /// Environment variables to set during the build. pub env: HashMap, - /// target triple + /// Target triple (e.g., "aarch64-unknown-none", "riscv64gc-unknown-none-elf"). pub target: String, - /// package name + /// Package name to build. pub package: String, - /// features to enable + /// Cargo features to enable. pub features: Vec, - /// log level feature + /// Log level feature to automatically enable. pub log: Option, - /// extra cargo .config.toml file - /// can be url or local path + /// Extra Cargo config file path or URL. + /// + /// Can be a local path or a URL (including GitHub URLs which are + /// automatically converted to raw content URLs). pub extra_config: Option, - /// other cargo args + /// Additional Cargo command-line arguments. pub args: Vec, - /// shell commands before build + /// Shell commands to run before the build. pub pre_build_cmds: Vec, - /// shell commands after build - /// `KERNEL_ELF` env var is set to the built ELF path + /// Shell commands to run after the build. + /// + /// The `KERNEL_ELF` environment variable is set to the built ELF path. pub post_build_cmds: Vec, - /// whether to output as binary + /// Whether to convert the ELF to raw binary format after building. pub to_bin: bool, } +/// Dependency configuration for feature management. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] pub struct Depend { + /// Dependency name. pub name: String, + /// Features to enable for this dependency. pub d_features: Vec, } +/// Log level configuration for the `log` crate. +/// +/// When specified, automatically enables the corresponding +/// `log/max_level_*` or `log/release_max_level_*` feature. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] pub enum LogLevel { + /// Trace level logging. Trace, + /// Debug level logging. Debug, + /// Info level logging. Info, + /// Warning level logging. Warn, + /// Error level logging. Error, } diff --git a/ostool/src/build/mod.rs b/ostool/src/build/mod.rs index 09c16df..39636c8 100644 --- a/ostool/src/build/mod.rs +++ b/ostool/src/build/mod.rs @@ -1,3 +1,23 @@ +//! Build system configuration and Cargo integration. +//! +//! This module provides functionality for building operating system projects +//! using Cargo or custom build commands. It supports: +//! +//! - Configuring build options via TOML configuration files +//! - Running pre-build and post-build shell commands +//! - Automatic feature detection and configuration +//! - Multiple runner types (QEMU, U-Boot) +//! +//! # Example +//! +//! ```rust,no_run +//! use ostool::build::config::{BuildConfig, BuildSystem, Cargo}; +//! use ostool::ctx::AppContext; +//! +//! // Build configurations are typically loaded from TOML files +//! // See .build.toml for example configuration format +//! ``` + use std::path::PathBuf; use anyhow::Context; @@ -10,21 +30,43 @@ use crate::{ ctx::AppContext, }; +/// Cargo builder implementation for building projects. pub mod cargo_builder; + +/// Build configuration types and structures. pub mod config; +/// Specifies the type of runner to use after building. +/// +/// This enum determines how the built artifact will be executed, +/// either through QEMU emulation or via U-Boot on real hardware. pub enum CargoRunnerKind { + /// Run the built artifact in QEMU emulator. Qemu { + /// Optional path to QEMU configuration file. qemu_config: Option, + /// Whether to enable debug mode (GDB server). debug: bool, + /// Whether to dump the device tree blob. dtb_dump: bool, }, + /// Run the built artifact on real hardware via U-Boot. Uboot { + /// Optional path to U-Boot configuration file. uboot_config: Option, }, } impl AppContext { + /// Builds the project using the specified build configuration. + /// + /// # Arguments + /// + /// * `config` - The build configuration specifying how to build the project. + /// + /// # Errors + /// + /// Returns an error if the build process fails. pub async fn build_with_config(&mut self, config: &config::BuildConfig) -> anyhow::Result<()> { match &config.system { config::BuildSystem::Custom(custom) => self.build_custom(custom)?, @@ -35,23 +77,65 @@ impl AppContext { Ok(()) } + /// Builds the project from the specified configuration file path. + /// + /// This is the main entry point for building projects. It loads the + /// configuration from the specified path (or default `.build.toml`) + /// and executes the build. + /// + /// # Arguments + /// + /// * `config_path` - Optional path to the build configuration file. + /// Defaults to `.build.toml` in the workspace directory. + /// + /// # Errors + /// + /// Returns an error if the configuration cannot be loaded or the build fails. pub async fn build(&mut self, config_path: Option) -> anyhow::Result<()> { let build_config = self.prepare_build_config(config_path, false).await?; println!("Build configuration: {:?}", build_config); self.build_with_config(&build_config).await } + /// Executes a custom build using shell commands. + /// + /// # Arguments + /// + /// * `config` - Custom build configuration containing the shell command. + /// + /// # Errors + /// + /// Returns an error if the shell command fails. pub fn build_custom(&mut self, config: &Custom) -> anyhow::Result<()> { self.shell_run_cmd(&config.build_cmd)?; Ok(()) } + /// Builds the project using Cargo. + /// + /// # Arguments + /// + /// * `config` - Cargo build configuration. + /// + /// # Errors + /// + /// Returns an error if the Cargo build fails. pub async fn cargo_build(&mut self, config: &Cargo) -> anyhow::Result<()> { cargo_builder::CargoBuilder::build_auto(self, config) .execute() .await } + /// Builds and runs the project using Cargo with the specified runner. + /// + /// # Arguments + /// + /// * `config` - Cargo build configuration. + /// * `runner` - The type of runner to use (QEMU or U-Boot). + /// + /// # Errors + /// + /// Returns an error if the build or run fails. pub async fn cargo_run( &mut self, config: &Cargo, diff --git a/ostool/src/ctx.rs b/ostool/src/ctx.rs index be7f1cd..4dfa6c3 100644 --- a/ostool/src/ctx.rs +++ b/ostool/src/ctx.rs @@ -1,3 +1,9 @@ +//! Application context and state management. +//! +//! This module provides the [`AppContext`] type which holds the global state +//! for the ostool application, including paths, build configuration, and +//! architecture information. + use std::{path::PathBuf, sync::Arc}; use anyhow::anyhow; @@ -15,31 +21,43 @@ use tokio::fs; use crate::build::config::BuildConfig; -/// Configuration for output directories (set from external config) +/// Configuration for output directories. +/// +/// Specifies where build outputs should be placed. #[derive(Default, Clone)] pub struct OutputConfig { + /// Custom build directory (overrides default `target/`). pub build_dir: Option, + /// Custom binary output directory. pub bin_dir: Option, } -/// Build artifacts (generated during build) +/// Build artifacts generated during the build process. #[derive(Default, Clone)] pub struct OutputArtifacts { + /// Path to the built ELF file. pub elf: Option, + /// Path to the converted binary file. pub bin: Option, } -/// Path configuration grouping all path-related fields +/// Path configuration grouping all path-related fields. #[derive(Default, Clone)] pub struct PathConfig { + /// Workspace root directory. pub workspace: PathBuf, + /// Cargo manifest directory. pub manifest: PathBuf, + /// Output directory configuration. pub config: OutputConfig, + /// Generated build artifacts. pub artifacts: OutputArtifacts, } impl PathConfig { - /// Get build directory, defaulting to manifest/target if not configured + /// Gets the build directory. + /// + /// Returns the configured build directory, or defaults to `manifest/target`. pub fn build_dir(&self) -> PathBuf { self.config .build_dir @@ -47,22 +65,44 @@ impl PathConfig { .unwrap_or_else(|| self.manifest.join("target")) } - /// Get bin directory, defaulting to build_dir if not configured + /// Gets the binary output directory if configured. pub fn bin_dir(&self) -> Option { self.config.bin_dir.clone() } } +/// The main application context holding all state. +/// +/// `AppContext` is the central state container for ostool operations. +/// It manages paths, build configuration, architecture detection, and +/// provides methods for building and running OS projects. #[derive(Default, Clone)] pub struct AppContext { + /// Path configuration for workspace, manifest, and outputs. pub paths: PathConfig, + /// Whether debug mode is enabled. pub debug: bool, + /// Detected CPU architecture from the ELF file. pub arch: Option, + /// Current build configuration. pub build_config: Option, + /// Path to the build configuration file. pub build_config_path: Option, } impl AppContext { + /// Executes a shell command in the current context. + /// + /// The command is run in the manifest directory with the `KERNEL_ELF` + /// environment variable set if an ELF artifact is available. + /// + /// # Arguments + /// + /// * `cmd` - The shell command to execute. + /// + /// # Errors + /// + /// Returns an error if the command fails to execute. pub fn shell_run_cmd(&self, cmd: &str) -> anyhow::Result<()> { let mut command = match std::env::consts::OS { "windows" => { @@ -88,6 +128,10 @@ impl AppContext { Ok(()) } + /// Creates a new command builder for the given program. + /// + /// The command is configured to run in the manifest directory with + /// variable substitution support. pub fn command(&self, program: &str) -> crate::utils::Command { let this = self.clone(); crate::utils::Command::new(program, &self.paths.manifest, move |s| { @@ -95,6 +139,11 @@ impl AppContext { }) } + /// Gets the Cargo metadata for the current workspace. + /// + /// # Errors + /// + /// Returns an error if `cargo metadata` fails. pub fn metadata(&self) -> anyhow::Result { let res = cargo_metadata::MetadataCommand::new() .current_dir(&self.paths.manifest) @@ -103,6 +152,9 @@ impl AppContext { Ok(res) } + /// Sets the ELF file path and detects its architecture. + /// + /// This also reads the ELF file to detect the target CPU architecture. pub async fn set_elf_path(&mut self, path: PathBuf) { self.paths.artifacts.elf = Some(path.clone()); let binary_data = match fs::read(path).await { @@ -122,6 +174,17 @@ impl AppContext { self.arch = Some(file.architecture()) } + /// Strips debug symbols from the ELF file. + /// + /// Creates a new `.elf` file with debug symbols stripped using `rust-objcopy`. + /// + /// # Returns + /// + /// Returns the path to the stripped ELF file. + /// + /// # Errors + /// + /// Returns an error if no ELF file is set or `rust-objcopy` fails. pub fn objcopy_elf(&mut self) -> anyhow::Result { let elf_path = self .paths @@ -165,6 +228,18 @@ impl AppContext { Ok(stripped_elf_path) } + /// Converts the ELF file to raw binary format. + /// + /// Uses `rust-objcopy` to convert the ELF file to a flat binary file + /// suitable for direct loading by bootloaders. + /// + /// # Returns + /// + /// Returns the path to the generated binary file. + /// + /// # Errors + /// + /// Returns an error if no ELF file is set or `rust-objcopy` fails. pub fn objcopy_output_bin(&mut self) -> anyhow::Result { if self.paths.artifacts.bin.is_some() { debug!("BIN file already exists: {:?}", self.paths.artifacts.bin); @@ -225,6 +300,20 @@ impl AppContext { Ok(bin_path) } + /// Loads and prepares the build configuration. + /// + /// This method loads the build configuration from a TOML file. If `menu` is + /// true, an interactive TUI is shown for configuration editing. + /// + /// # Arguments + /// + /// * `config_path` - Optional path to the configuration file. Defaults to + /// `.build.toml` in the workspace directory. + /// * `menu` - If true, shows an interactive configuration menu. + /// + /// # Errors + /// + /// Returns an error if the configuration file cannot be loaded or parsed. pub async fn prepare_build_config( &mut self, config_path: Option, @@ -250,6 +339,10 @@ impl AppContext { Ok(c) } + /// Replaces variable placeholders in a string. + /// + /// Currently supports `${workspaceFolder}` which is replaced with the + /// workspace directory path. pub fn value_replace_with_var(&self, value: S) -> String where S: AsRef, @@ -261,6 +354,9 @@ impl AppContext { ) } + /// Returns UI hooks for the configuration editor. + /// + /// These hooks provide interactive selection dialogs for features and packages. pub fn ui_hocks(&self) -> Vec { vec![self.ui_hock_feature_select(), self.ui_hock_pacage_select()] } diff --git a/ostool/src/lib.rs b/ostool/src/lib.rs index 57ac6e4..d3071f6 100644 --- a/ostool/src/lib.rs +++ b/ostool/src/lib.rs @@ -1,10 +1,65 @@ +//! # ostool +//! +//! A comprehensive toolkit for operating system development. +//! +//! `ostool` provides utilities for building, running, and debugging operating systems, +//! with special support for embedded systems and bootloader interaction. +//! +//! ## Features +//! +//! - **Build System**: Cargo-based build configuration with customizable options +//! - **QEMU Integration**: Easy QEMU launching with various architectures support +//! - **U-Boot Support**: Serial communication and file transfer via YMODEM +//! - **TFTP Server**: Built-in TFTP server for network booting +//! - **Menu Configuration**: TUI-based configuration editor (like Linux kernel's menuconfig) +//! - **Serial Terminal**: Interactive serial terminal for device communication +//! +//! ## Modules +//! +//! - [`build`] - Build system configuration and Cargo integration +//! - [`ctx`] - Application context and state management +//! - [`menuconfig`] - TUI-based menu configuration +//! - [`run`] - QEMU, TFTP, and U-Boot runners +//! - [`sterm`] - Serial terminal implementation +//! - [`utils`] - Common utilities and helper functions +//! +//! ## Example +//! +//! ```rust,no_run +//! // ostool is primarily used as a CLI tool +//! // See the binary targets for usage examples +//! ``` + #![cfg(not(target_os = "none"))] +/// Build system configuration and Cargo integration. +/// +/// Provides functionality for configuring and executing Cargo builds +/// with custom options and target specifications. pub mod build; + +/// Application context and state management. pub mod ctx; + +/// TUI-based menu configuration system. +/// +/// Similar to Linux kernel's menuconfig, allows users to configure +/// build options through an interactive terminal interface. pub mod menuconfig; + +/// Runtime execution modules for QEMU, TFTP, and U-Boot. +/// +/// Contains implementations for launching QEMU instances, +/// running TFTP servers, and communicating with U-Boot. pub mod run; + +/// Serial terminal implementation. +/// +/// Provides an interactive serial terminal for communication +/// with embedded devices and development boards. pub mod sterm; + +/// Common utilities and helper functions. pub mod utils; #[macro_use] diff --git a/ostool/src/menuconfig.rs b/ostool/src/menuconfig.rs index 5de237c..6ec7fbf 100644 --- a/ostool/src/menuconfig.rs +++ b/ostool/src/menuconfig.rs @@ -1,3 +1,13 @@ +//! TUI-based menu configuration system. +//! +//! This module provides an interactive terminal user interface for configuring +//! build options, similar to Linux kernel's menuconfig. It supports editing +//! configuration for: +//! +//! - Build settings (`.build.toml`) +//! - QEMU settings (`.qemu.toml`) +//! - U-Boot settings (`.uboot.toml`) + use anyhow::Result; use clap::ValueEnum; use log::info; @@ -7,15 +17,30 @@ use crate::ctx::AppContext; use crate::run::qemu::QemuConfig; use crate::run::uboot::UbootConfig; +/// Menu configuration mode selector. #[derive(ValueEnum, Clone, Debug)] pub enum MenuConfigMode { + /// Configure QEMU runner settings. Qemu, + /// Configure U-Boot runner settings. Uboot, } +/// Handler for menu configuration operations. pub struct MenuConfigHandler; impl MenuConfigHandler { + /// Handles the menu configuration command. + /// + /// # Arguments + /// + /// * `ctx` - The application context. + /// * `mode` - Optional mode specifying which configuration to edit. + /// If `None`, shows the default build configuration menu. + /// + /// # Errors + /// + /// Returns an error if the configuration cannot be loaded or saved. pub async fn handle_menuconfig( ctx: &mut AppContext, mode: Option, diff --git a/ostool/src/run/mod.rs b/ostool/src/run/mod.rs index 41aefae..65260fc 100644 --- a/ostool/src/run/mod.rs +++ b/ostool/src/run/mod.rs @@ -1,5 +1,20 @@ +//! Runtime execution modules for QEMU, TFTP, and U-Boot. +//! +//! This module contains implementations for running operating systems +//! in various environments: +//! +//! - [`qemu`] - Running in QEMU emulator with UEFI support +//! - [`tftp`] - TFTP server for network booting +//! - [`uboot`] - U-Boot bootloader integration via serial/YMODEM + +/// QEMU emulator runner with UEFI/OVMF support. pub mod qemu; + +/// TFTP server for network booting. pub mod tftp; + +/// U-Boot bootloader integration. pub mod uboot; +/// OVMF prebuilt firmware downloader (internal). mod ovmf_prebuilt; diff --git a/ostool/src/run/qemu.rs b/ostool/src/run/qemu.rs index 0c674c9..aa7619a 100644 --- a/ostool/src/run/qemu.rs +++ b/ostool/src/run/qemu.rs @@ -1,3 +1,25 @@ +//! QEMU emulator runner with UEFI/OVMF support. +//! +//! This module provides functionality for running operating systems in QEMU +//! with support for: +//! +//! - Multiple architectures (x86_64, aarch64, riscv64, etc.) +//! - UEFI boot via OVMF firmware +//! - Debug mode with GDB server +//! - Output pattern matching for test automation +//! +//! # Configuration +//! +//! QEMU configuration is stored in `.qemu.toml` files: +//! +//! ```toml +//! args = ["-nographic", "-cpu", "cortex-a53"] +//! uefi = false +//! to_bin = true +//! success_regex = ["All tests passed"] +//! fail_regex = ["PANIC", "FAILED"] +//! ``` + use std::{ ffi::OsString, io::{BufReader, Read}, @@ -19,23 +41,47 @@ use crate::{ run::ovmf_prebuilt::{Arch, FileType, Prebuilt, Source}, }; +/// QEMU configuration structure. +/// +/// This configuration is typically loaded from a `.qemu.toml` file. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Default)] pub struct QemuConfig { + /// Additional QEMU command-line arguments. pub args: Vec, + /// Whether to use UEFI boot via OVMF firmware. pub uefi: bool, - /// objcopy output as binary + /// Whether to convert ELF to raw binary before loading. pub to_bin: bool, + /// Regex patterns that indicate successful execution. pub success_regex: Vec, + /// Regex patterns that indicate failed execution. pub fail_regex: Vec, } +/// Arguments for running QEMU. #[derive(Debug, Clone)] pub struct RunQemuArgs { + /// Optional path to QEMU configuration file. pub qemu_config: Option, + /// Whether to dump the device tree blob. pub dtb_dump: bool, + /// Whether to show QEMU output. pub show_output: bool, } +/// Runs the operating system in QEMU. +/// +/// This function configures and launches QEMU with the appropriate settings +/// based on the detected architecture and configuration file. +/// +/// # Arguments +/// +/// * `ctx` - The application context containing paths and build artifacts. +/// * `args` - QEMU run arguments. +/// +/// # Errors +/// +/// Returns an error if QEMU fails to start or exits with an error. pub async fn run_qemu(ctx: AppContext, args: RunQemuArgs) -> anyhow::Result<()> { // Build logic will be implemented here let config_path = match args.qemu_config.clone() { diff --git a/ostool/src/run/tftp.rs b/ostool/src/run/tftp.rs index 23719d6..d445dca 100644 --- a/ostool/src/run/tftp.rs +++ b/ostool/src/run/tftp.rs @@ -1,3 +1,17 @@ +//! TFTP server for network booting. +//! +//! This module provides a simple TFTP server for network booting scenarios, +//! typically used with U-Boot to transfer kernel images over the network. +//! +//! # Permissions +//! +//! The TFTP server binds to port 69, which requires elevated privileges. +//! On Linux, you can grant the necessary capabilities with: +//! +//! ```bash +//! sudo setcap cap_net_bind_service=+eip $(which ostool) +//! ``` + use std::net::{IpAddr, Ipv4Addr}; use colored::Colorize as _; @@ -5,6 +19,19 @@ use tftpd::{Config, Server}; use crate::ctx::AppContext; +/// Starts a TFTP server serving files from the build output directory. +/// +/// The server runs in a background thread and serves files from the directory +/// containing the ELF/binary artifacts. +/// +/// # Arguments +/// +/// * `app` - The application context containing the file paths. +/// +/// # Errors +/// +/// Returns an error if the server fails to start (e.g., port already in use +/// or insufficient permissions). pub fn run_tftp_server(app: &AppContext) -> anyhow::Result<()> { // TFTP server implementation goes here let mut file_dir = app.paths.manifest.clone(); diff --git a/ostool/src/sterm/mod.rs b/ostool/src/sterm/mod.rs index 7cd10e0..eb7c1da 100644 --- a/ostool/src/sterm/mod.rs +++ b/ostool/src/sterm/mod.rs @@ -1,3 +1,16 @@ +//! Serial terminal implementation. +//! +//! This module provides an interactive serial terminal for communication +//! with embedded devices and development boards. It supports: +//! +//! - Full keyboard input with special key sequences +//! - Line-based output callback for pattern matching +//! - Raw terminal mode for proper character handling +//! +//! # Exit Sequence +//! +//! Press `Ctrl+A` followed by `x` to exit the serial terminal. + use std::io::{self, Read, Write}; use std::sync::atomic::AtomicBool; use std::sync::{Arc, Mutex}; @@ -15,22 +28,43 @@ type Tx = Box; type Rx = Box; type OnlineCallback = Box; +/// Interactive serial terminal. +/// +/// `SerialTerm` provides a bidirectional terminal interface over serial ports, +/// allowing users to interact with embedded devices in real-time. +/// +/// # Example +/// +/// ```rust,no_run +/// use ostool::sterm::SerialTerm; +/// +/// // SerialTerm is typically used with serial port connections +/// // See run::uboot for usage examples +/// ``` pub struct SerialTerm { tx: Arc>, rx: Arc>, on_line: Option, } +/// Handle for controlling the terminal session. +/// +/// Provides methods to stop the terminal from within callbacks. pub struct TermHandle { is_running: AtomicBool, } impl TermHandle { + /// Stops the terminal session. + /// + /// This can be called from within a line callback to terminate the session + /// when a specific pattern is detected. pub fn stop(&self) { self.is_running .store(false, std::sync::atomic::Ordering::Release); } + /// Returns whether the terminal session is still running. pub fn is_running(&self) -> bool { self.is_running.load(std::sync::atomic::Ordering::Acquire) } @@ -44,6 +78,13 @@ enum KeySequenceState { } impl SerialTerm { + /// Creates a new serial terminal with the given read/write streams. + /// + /// # Arguments + /// + /// * `tx` - Writer for sending data to the serial port. + /// * `rx` - Reader for receiving data from the serial port. + /// * `on_line` - Callback function invoked for each complete line received. pub fn new(tx: Tx, rx: Rx, on_line: F) -> Self where F: Fn(&TermHandle, &str) + Send + Sync + 'static, @@ -55,6 +96,14 @@ impl SerialTerm { } } + /// Runs the interactive serial terminal. + /// + /// This method blocks until the user exits (Ctrl+A x) or the line callback + /// calls `TermHandle::stop()`. + /// + /// # Errors + /// + /// Returns an error if terminal mode cannot be set or I/O fails. pub async fn run(&mut self) -> anyhow::Result<()> { // 启用raw模式 diff --git a/ostool/src/utils.rs b/ostool/src/utils.rs index 2798f21..7fddb9b 100644 --- a/ostool/src/utils.rs +++ b/ostool/src/utils.rs @@ -1,3 +1,8 @@ +//! Common utilities and helper functions. +//! +//! This module provides utility types and functions used throughout ostool, +//! including command execution helpers and string processing utilities. + use std::{ ffi::OsStr, ops::{Deref, DerefMut}, @@ -7,6 +12,10 @@ use std::{ use anyhow::bail; use colored::Colorize; +/// A command builder wrapper with variable substitution support. +/// +/// `Command` wraps `std::process::Command` and adds support for automatic +/// variable replacement in arguments and environment values. pub struct Command { inner: std::process::Command, value_replace: Box String>, @@ -27,6 +36,13 @@ impl DerefMut for Command { } impl Command { + /// Creates a new command builder. + /// + /// # Arguments + /// + /// * `program` - The program to execute. + /// * `workdir` - The working directory for the command. + /// * `value_replace` - Function to perform variable substitution on arguments. pub fn new( program: S, workdir: &Path, @@ -45,6 +61,7 @@ impl Command { } } + /// Prints the command to stdout with colored formatting. pub fn print_cmd(&self) { let mut cmd_str = self.get_program().to_string_lossy().to_string(); @@ -56,6 +73,11 @@ impl Command { println!("{}", cmd_str.purple().bold()); } + /// Executes the command and waits for it to complete. + /// + /// # Errors + /// + /// Returns an error if the command fails to execute or exits with non-zero status. pub fn run(&mut self) -> anyhow::Result<()> { self.print_cmd(); let status = self.status()?; @@ -65,6 +87,7 @@ impl Command { Ok(()) } + /// Adds an argument to the command with variable substitution. pub fn arg(&mut self, arg: S) -> &mut Command where S: AsRef, @@ -74,6 +97,7 @@ impl Command { self } + /// Adds multiple arguments to the command with variable substitution. pub fn args(&mut self, args: I) -> &mut Command where I: IntoIterator, @@ -85,6 +109,7 @@ impl Command { self } + /// Sets an environment variable for the command with variable substitution. pub fn env(&mut self, key: K, val: V) -> &mut Command where K: AsRef, @@ -144,6 +169,21 @@ impl Command { // Ok(config) // } +/// Replaces environment variable placeholders in a string. +/// +/// Placeholders use the format `${env:VAR_NAME}` where `VAR_NAME` is the +/// name of an environment variable. If the variable is not set, the +/// placeholder is replaced with an empty string. +/// +/// # Example +/// +/// ```rust +/// use ostool::utils::replace_env_placeholders; +/// +/// unsafe { std::env::set_var("MY_VAR", "hello"); } +/// let result = replace_env_placeholders("Value: ${env:MY_VAR}").unwrap(); +/// assert_eq!(result, "Value: hello"); +/// ``` pub fn replace_env_placeholders(input: &str) -> anyhow::Result { use std::env; diff --git a/uboot-shell/Cargo.toml b/uboot-shell/Cargo.toml index ae86954..d7acb92 100644 --- a/uboot-shell/Cargo.toml +++ b/uboot-shell/Cargo.toml @@ -2,12 +2,14 @@ authors = ["周睿 "] categories = ["os", "embedded", "development-tools"] description = "A crate for communicating with u-boot" +documentation = "https://docs.rs/uboot-shell" edition = "2024" -keywords = ["u-boot", "shell", "embedded"] +keywords = ["u-boot", "shell", "embedded", "serial", "ymodem"] license = "MIT" name = "uboot-shell" -repository = "https://github.com/ZR233/ostool/ostool" -version = "0.2.0" +readme = "README.md" +repository = "https://github.com/ZR233/ostool" +version = "0.2.1" [dependencies] colored = "3" diff --git a/uboot-shell/README.md b/uboot-shell/README.md index b527ff3..bef3423 100644 --- a/uboot-shell/README.md +++ b/uboot-shell/README.md @@ -5,6 +5,8 @@ A crate for communicating with u-boot. ## Usage ```rust +use uboot_shell::UbootShell; + let port = "/dev/ttyUSB0"; let baud = 115200; let rx = serialport::new(port, baud) @@ -12,7 +14,7 @@ let rx = serialport::new(port, baud) .unwrap(); let tx = rx.try_clone().unwrap(); println!("wait for u-boot shell..."); -let mut uboot = UbootShell::new(tx, rx); +let mut uboot = UbootShell::new(tx, rx).unwrap(); println!("u-boot shell ready"); let res = uboot.cmd("help").unwrap(); println!("{}", res); diff --git a/uboot-shell/src/crc.rs b/uboot-shell/src/crc.rs index 0e5a092..ccb5251 100644 --- a/uboot-shell/src/crc.rs +++ b/uboot-shell/src/crc.rs @@ -1,4 +1,9 @@ -/* Table of CRC constants - implements x^16+x^12+x^5+1 */ +//! CRC16-CCITT checksum implementation. +//! +//! This module provides CRC16-CCITT checksum calculation used by the YMODEM protocol. +//! The polynomial used is x^16 + x^12 + x^5 + 1 (0x1021). + +/// CRC16-CCITT lookup table - implements polynomial x^16+x^12+x^5+1 const CRC16_TAB: &[u16] = &[ 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, @@ -24,6 +29,25 @@ const CRC16_TAB: &[u16] = &[ 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, ]; +/// Calculates CRC16-CCITT checksum for the given data. +/// +/// # Arguments +/// +/// * `cksum` - Initial checksum value (usually 0) +/// * `buf` - Data buffer to calculate checksum for +/// +/// # Returns +/// +/// The calculated CRC16-CCITT checksum value. +/// +/// # Example +/// +/// ```rust +/// use uboot_shell::crc::crc16_ccitt; +/// +/// let data = b"Hello, World!"; +/// let checksum = crc16_ccitt(0, data); +/// ``` pub fn crc16_ccitt(mut cksum: u16, buf: &[u8]) -> u16 { for &byte in buf { cksum = CRC16_TAB[((cksum >> 8) ^ byte as u16) as usize] ^ (cksum << 8); diff --git a/uboot-shell/src/lib.rs b/uboot-shell/src/lib.rs index f636a45..faf23f3 100644 --- a/uboot-shell/src/lib.rs +++ b/uboot-shell/src/lib.rs @@ -1,3 +1,53 @@ +//! # uboot-shell +//! +//! A Rust library for communicating with U-Boot bootloader over serial connection. +//! +//! This crate provides functionality to interact with U-Boot shell, execute commands, +//! transfer files via YMODEM protocol, and manage environment variables. +//! +//! ## Features +//! +//! - Automatic U-Boot shell detection and synchronization +//! - Command execution with retry support +//! - YMODEM file transfer protocol implementation +//! - Environment variable management +//! - CRC16-CCITT checksum support +//! +//! ## Quick Start +//! +//! ```rust,no_run +//! use uboot_shell::UbootShell; +//! use std::io::{Read, Write}; +//! +//! // Open serial port (using serialport crate) +//! let port = serialport::new("/dev/ttyUSB0", 115200) +//! .open() +//! .unwrap(); +//! let rx = port.try_clone().unwrap(); +//! let tx = port; +//! +//! // Create U-Boot shell instance (blocks until shell is ready) +//! let mut uboot = UbootShell::new(tx, rx).unwrap(); +//! +//! // Execute commands +//! let output = uboot.cmd("help").unwrap(); +//! println!("{}", output); +//! +//! // Get/set environment variables +//! let bootargs = uboot.env("bootargs").unwrap(); +//! uboot.set_env("myvar", "myvalue").unwrap(); +//! +//! // Transfer file via YMODEM +//! uboot.loady(0x80000000, "kernel.bin", |sent, total| { +//! println!("Progress: {}/{}", sent, total); +//! }).unwrap(); +//! ``` +//! +//! ## Modules +//! +//! - [`crc`] - CRC16-CCITT checksum implementation +//! - [`ymodem`] - YMODEM file transfer protocol + #[macro_use] extern crate log; @@ -13,8 +63,11 @@ use std::{ time::{Duration, Instant}, }; -mod crc; -mod ymodem; +/// CRC16-CCITT checksum implementation. +pub mod crc; + +/// YMODEM file transfer protocol implementation. +pub mod ymodem; macro_rules! dbg { ($($arg:tt)*) => {{ @@ -26,14 +79,62 @@ const CTRL_C: u8 = 0x03; const INT_STR: &str = ""; const INT: &[u8] = INT_STR.as_bytes(); +/// U-Boot shell communication interface. +/// +/// `UbootShell` provides methods to interact with U-Boot bootloader +/// over a serial connection. It handles shell synchronization, +/// command execution, and file transfers. +/// +/// # Example +/// +/// ```rust,no_run +/// use uboot_shell::UbootShell; +/// +/// // Assuming tx and rx are Read/Write implementations +/// # fn example(tx: impl std::io::Write + Send + 'static, rx: impl std::io::Read + Send + 'static) { +/// let mut shell = UbootShell::new(tx, rx).unwrap(); +/// let result = shell.cmd("printenv").unwrap(); +/// # } +/// ``` pub struct UbootShell { + /// Transmit channel for sending data to U-Boot. pub tx: Option>, + /// Receive channel for reading data from U-Boot. pub rx: Option>, + /// Shell prompt prefix detected during initialization. perfix: String, } impl UbootShell { - /// Create a new UbootShell instance, block wait for uboot shell. + /// Creates a new UbootShell instance and waits for U-Boot shell to be ready. + /// + /// This function will block until it successfully detects the U-Boot shell prompt. + /// It sends interrupt signals (Ctrl+C) to ensure the shell is in a clean state. + /// + /// # Arguments + /// + /// * `tx` - A writable stream for sending data to U-Boot + /// * `rx` - A readable stream for receiving data from U-Boot + /// + /// # Returns + /// + /// Returns `Ok(UbootShell)` if the shell is successfully initialized, + /// or an `Err` if communication fails. + /// + /// # Errors + /// + /// Returns an error if the serial I/O fails or the prompt cannot be detected + /// within the internal retry loop. + /// + /// # Example + /// + /// ```rust,no_run + /// use uboot_shell::UbootShell; + /// + /// let port = serialport::new("/dev/ttyUSB0", 115200).open().unwrap(); + /// let rx = port.try_clone().unwrap(); + /// let mut uboot = UbootShell::new(port, rx).unwrap(); + /// ``` pub fn new(tx: impl Write + Send + 'static, rx: impl Read + Send + 'static) -> Result { let mut s = Self { tx: Some(Box::new(tx)), @@ -143,6 +244,21 @@ impl UbootShell { } } + /// Waits for a specific string to appear in the U-Boot output. + /// + /// Reads from the serial connection until the specified string is found. + /// + /// # Arguments + /// + /// * `val` - The string to wait for + /// + /// # Returns + /// + /// Returns the accumulated output up to and including the matched string. + /// + /// # Errors + /// + /// Returns an error when the underlying read operation times out or fails. pub fn wait_for_reply(&mut self, val: &str) -> Result { let mut reply = Vec::new(); let mut display = Vec::new(); @@ -167,6 +283,18 @@ impl UbootShell { .to_string()) } + /// Sends a command to U-Boot without waiting for the response. + /// + /// This is useful for commands that don't produce output or when + /// you want to handle the response separately. + /// + /// # Arguments + /// + /// * `cmd` - The command string to send + /// + /// # Errors + /// + /// Returns any I/O error that occurs while writing to the serial stream. pub fn cmd_without_reply(&mut self, cmd: &str) -> Result<()> { self.tx().write_all(cmd.as_bytes())?; self.tx().write_all("\n".as_bytes())?; @@ -204,6 +332,34 @@ impl UbootShell { } } + /// Executes a command in U-Boot shell and returns the output. + /// + /// This method sends the command, waits for completion, and verifies + /// that the command executed successfully. It includes automatic retry + /// logic (up to 3 attempts) for improved reliability. + /// + /// # Arguments + /// + /// * `cmd` - The command string to execute + /// + /// # Returns + /// + /// Returns `Ok(String)` with the command output on success, + /// or an `Err` if the command fails after all retries. + /// + /// # Errors + /// + /// Returns an error if the command fails after retries or if serial I/O fails. + /// + /// # Example + /// + /// ```rust,no_run + /// # use uboot_shell::UbootShell; + /// # fn example(uboot: &mut UbootShell) { + /// let output = uboot.cmd("printenv bootargs").unwrap(); + /// println!("bootargs: {}", output); + /// # } + /// ``` pub fn cmd(&mut self, cmd: &str) -> Result { info!("cmd: {cmd}"); let mut retry = 3; @@ -222,11 +378,56 @@ impl UbootShell { ))) } + /// Sets a U-Boot environment variable. + /// + /// # Arguments + /// + /// * `name` - The name of the environment variable + /// * `value` - The value to set + /// + /// # Example + /// + /// ```rust,no_run + /// # use uboot_shell::UbootShell; + /// # fn example(uboot: &mut UbootShell) { + /// uboot.set_env("bootargs", "console=ttyS0,115200").unwrap(); + /// # } + /// ``` + /// + /// # Errors + /// + /// Returns any error from the underlying command execution. pub fn set_env(&mut self, name: impl Into, value: impl Into) -> Result<()> { self.cmd(&format!("setenv {} {}", name.into(), value.into()))?; Ok(()) } + /// Gets the value of a U-Boot environment variable. + /// + /// # Arguments + /// + /// * `name` - The name of the environment variable + /// + /// # Returns + /// + /// Returns `Ok(String)` with the variable value, or an `Err` if not found. + /// + /// # Errors + /// + /// Returns `ErrorKind::NotFound` if the variable is not set or cannot be read. + /// + /// # Example + /// + /// ```rust,no_run + /// # use uboot_shell::UbootShell; + /// # fn example(uboot: &mut UbootShell) { + /// let bootargs = uboot.env("bootargs").unwrap(); + /// # } + /// ``` + /// + /// # Errors + /// + /// Returns `ErrorKind::NotFound` if the variable is not set or cannot be read. pub fn env(&mut self, name: impl Into) -> Result { let name = name.into(); let s = self.cmd(&format!("echo ${}", name))?; @@ -244,6 +445,22 @@ impl UbootShell { Ok(s) } + /// Gets a U-Boot environment variable as an integer. + /// + /// Supports both decimal and hexadecimal (0x prefix) formats. + /// + /// # Arguments + /// + /// * `name` - The name of the environment variable + /// + /// # Returns + /// + /// Returns `Ok(usize)` with the parsed integer value, + /// or an `Err` if not found or not a valid number. + /// + /// # Errors + /// + /// Returns `ErrorKind::InvalidData` if the value is not a valid integer. pub fn env_int(&mut self, name: impl Into) -> Result { let name = name.into(); let line = self.env(&name)?; @@ -255,6 +472,36 @@ impl UbootShell { )) } + /// Transfers a file to U-Boot memory using YMODEM protocol. + /// + /// Uses the U-Boot `loady` command to receive files via YMODEM protocol. + /// The file will be loaded to the specified memory address. + /// + /// # Arguments + /// + /// * `addr` - The memory address where the file will be loaded + /// * `file` - Path to the file to transfer + /// * `on_progress` - Callback function called with (bytes_sent, total_bytes) + /// + /// # Returns + /// + /// Returns `Ok(String)` with the U-Boot response on success. + /// + /// # Errors + /// + /// Returns an error if the file cannot be opened, the path has a non-UTF-8 + /// file name, or if the serial transfer fails. + /// + /// # Example + /// + /// ```rust,no_run + /// # use uboot_shell::UbootShell; + /// # fn example(uboot: &mut UbootShell) { + /// uboot.loady(0x80000000, "kernel.bin", |sent, total| { + /// println!("Progress: {}/{} bytes", sent, total); + /// }).unwrap(); + /// # } + /// ``` pub fn loady( &mut self, addr: usize, @@ -266,11 +513,14 @@ impl UbootShell { let mut p = ymodem::Ymodem::new(crc); let file = file.into(); - let name = file.file_name().unwrap().to_str().unwrap(); + let name = file + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| Error::new(ErrorKind::InvalidInput, "file name must be valid UTF-8"))?; - let mut file = File::open(&file).unwrap(); + let mut file = File::open(&file)?; - let size = file.metadata().unwrap().len() as usize; + let size = file.metadata()?.len() as usize; p.send(self, &mut file, name, size, |p| { on_progress(p, size); @@ -291,7 +541,10 @@ impl UbootShell { } let res = String::from_utf8_lossy(&reply); if res.contains("try 'help'") { - panic!("{}", res); + return Err(Error::new( + ErrorKind::InvalidData, + format!("U-Boot loady failed: {res}"), + )); } } } diff --git a/uboot-shell/src/ymodem.rs b/uboot-shell/src/ymodem.rs index 1eae1f1..c2bd3b8 100644 --- a/uboot-shell/src/ymodem.rs +++ b/uboot-shell/src/ymodem.rs @@ -1,23 +1,57 @@ +//! YMODEM file transfer protocol implementation. +//! +//! This module implements the YMODEM protocol for file transfers over serial connections. +//! YMODEM is commonly used by U-Boot's `loady` command. +//! +//! ## Protocol Overview +//! +//! YMODEM transfers files in 128 or 1024 byte blocks with CRC16 error checking. +//! The protocol supports: +//! +//! - File name and size transmission in the first block +//! - Automatic block size selection (128 or 1024 bytes) +//! - CRC16-CCITT or checksum error detection +//! - Retry mechanism for failed transmissions + use std::io::*; use crate::crc::crc16_ccitt; +/// Start of Header - 128 byte block const SOH: u8 = 0x01; +/// Start of Text - 1024 byte block const STX: u8 = 0x02; +/// End of Transmission const EOT: u8 = 0x04; +/// Acknowledge const ACK: u8 = 0x06; +/// Negative Acknowledge const NAK: u8 = 0x15; -// const CAN: u8 = 0x18; +// const CAN: u8 = 0x18; // Cancel +/// End of File padding character const EOF: u8 = 0x1A; +/// CRC mode request character const CRC: u8 = 0x43; +/// YMODEM protocol handler for file transfers. +/// +/// Implements the YMODEM protocol for sending files over serial connections. +/// Supports both CRC16 and checksum modes. pub struct Ymodem { + /// Whether to use CRC16 mode (true) or checksum mode (false) crc_mode: bool, + /// Current block number blk: u8, + /// Number of remaining retry attempts retries: usize, } impl Ymodem { + /// Creates a new YMODEM sender. + /// + /// # Arguments + /// + /// * `crc_mode` - Whether to start in CRC16 mode (`true`) or checksum mode (`false`) pub fn new(crc_mode: bool) -> Self { Self { crc_mode, @@ -52,6 +86,19 @@ impl Ymodem { } } + /// Sends a file over the YMODEM protocol. + /// + /// # Arguments + /// + /// * `dev` - The device implementing `Read + Write` (serial stream) + /// * `file` - The readable file stream + /// * `name` - File name reported to the receiver + /// * `size` - File size in bytes + /// * `on_progress` - Callback invoked with the total bytes sent so far + /// + /// # Errors + /// + /// Returns any I/O error from the underlying device or file stream. pub fn send( &mut self, dev: &mut D,