From nobody Fri Oct 10 13:34:15 2025 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-1.web.codeaurora.org [10.30.226.201]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 965DB2EACFE; Sat, 14 Jun 2025 13:49:18 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=10.30.226.201 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1749908958; cv=none; b=s6VEDrYAZJGz1yl9EP6Ty15zlsNNco1yBIysl0SPUrfNABI9Z7i0SQuwPjVt/KnDUoMvYHdUT82v55WENrLZmPmrOXoa5YCZfdxrJfDg4/lxZAKUdiQZiDN3J/HcoWZOopdrzJ6HsTzKL+33ZA/vFxT4xFWiR8P/lpU6KLJo2D4= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1749908958; c=relaxed/simple; bh=G4abd4n8OMhquGl8B0vC4XK2aT/vUVMRQ4+VJ7cqekM=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=EZRXJSybwak5467+zVjilCN3G/zB1Ea1p/jsAjsnTOoHWb2pIThNt2hsiZFVTcK6WGRyTinRFLjlF0mO/Njehwin6oLnxrXOpZoKX/s1xxxTktxjzpHD375+vm7pRokhS7fxdyOdRq4IMPVlk9D9IS5IwUXbUvou7XE9qmwYh4M= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=teM5oIFY; arc=none smtp.client-ip=10.30.226.201 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="teM5oIFY" Received: by smtp.kernel.org (Postfix) with ESMTPSA id BD81FC4CEEB; Sat, 14 Jun 2025 13:49:17 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1749908958; bh=G4abd4n8OMhquGl8B0vC4XK2aT/vUVMRQ4+VJ7cqekM=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=teM5oIFYMQGExhd5Njc/ditHNtwUwxkUK17fEJfuCvrEee6KWZI+8o3jh3z6Vyp0k wz8sDtf8p0Pxd25T7WYyOIlTEsCq8lDEHmEyxA0V6n5I0ohXnE2Il6910Km0gBLxdv EHuj4eAtXxj1zlpAZ5at/SvoOf7mub+MWj/EfV4+YTEByrfxXSbq7/FZQYOM/59rNn F/hiK/zKbRRVI5cBjEwhuvf+rUo9C728jaubiOAw6LB26PqO6d3kn2acMpoTlPRhDQ +9SF6klx43sRhfjXTqe7vR4WhF0sWPLIzJTn77hjevvDsBpVT6IFYg1ptvl326m+xZ dZbwAeYU34+WQ== From: Sasha Levin To: linux-kernel@vger.kernel.org Cc: linux-api@vger.kernel.org, workflows@vger.kernel.org, tools@kernel.org, Sasha Levin Subject: [RFC 19/19] tools/kapi: Add kernel API specification extraction tool Date: Sat, 14 Jun 2025 09:48:58 -0400 Message-Id: <20250614134858.790460-20-sashal@kernel.org> X-Mailer: git-send-email 2.39.5 In-Reply-To: <20250614134858.790460-1-sashal@kernel.org> References: <20250614134858.790460-1-sashal@kernel.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" The kapi tool extracts and displays kernel API specifications. Signed-off-by: Sasha Levin --- Documentation/admin-guide/kernel-api-spec.rst | 198 ++++++- tools/kapi/.gitignore | 4 + tools/kapi/Cargo.toml | 19 + tools/kapi/src/extractor/debugfs.rs | 204 ++++++++ tools/kapi/src/extractor/mod.rs | 95 ++++ tools/kapi/src/extractor/source_parser.rs | 488 ++++++++++++++++++ .../src/extractor/vmlinux/binary_utils.rs | 130 +++++ tools/kapi/src/extractor/vmlinux/mod.rs | 372 +++++++++++++ tools/kapi/src/formatter/json.rs | 170 ++++++ tools/kapi/src/formatter/mod.rs | 68 +++ tools/kapi/src/formatter/plain.rs | 99 ++++ tools/kapi/src/formatter/rst.rs | 144 ++++++ tools/kapi/src/main.rs | 121 +++++ 13 files changed, 2109 insertions(+), 3 deletions(-) create mode 100644 tools/kapi/.gitignore create mode 100644 tools/kapi/Cargo.toml create mode 100644 tools/kapi/src/extractor/debugfs.rs create mode 100644 tools/kapi/src/extractor/mod.rs create mode 100644 tools/kapi/src/extractor/source_parser.rs create mode 100644 tools/kapi/src/extractor/vmlinux/binary_utils.rs create mode 100644 tools/kapi/src/extractor/vmlinux/mod.rs create mode 100644 tools/kapi/src/formatter/json.rs create mode 100644 tools/kapi/src/formatter/mod.rs create mode 100644 tools/kapi/src/formatter/plain.rs create mode 100644 tools/kapi/src/formatter/rst.rs create mode 100644 tools/kapi/src/main.rs diff --git a/Documentation/admin-guide/kernel-api-spec.rst b/Documentation/= admin-guide/kernel-api-spec.rst index 3a63f6711e27b..9b452753111ad 100644 --- a/Documentation/admin-guide/kernel-api-spec.rst +++ b/Documentation/admin-guide/kernel-api-spec.rst @@ -31,7 +31,9 @@ The framework aims to: common programming errors during development and testing. =20 3. **Support Tooling**: Export API specifications in machine-readable form= ats for - use by static analyzers, documentation generators, and development tool= s. + use by static analyzers, documentation generators, and development tool= s. The + ``kapi`` tool (see `The kapi Tool`_) provides comprehensive extraction = and + formatting capabilities. =20 4. **Enhance Debugging**: Provide detailed API information at runtime thro= ugh debugfs for debugging and introspection. @@ -71,6 +73,13 @@ The framework consists of several key components: - Type-safe parameter specifications - Context and constraint definitions =20 +5. **kapi Tool** (``tools/kapi/``) + + - Userspace utility for extracting specifications + - Multiple input sources (source, binary, debugfs) + - Multiple output formats (plain, JSON, RST) + - Testing and validation utilities + Data Model ---------- =20 @@ -344,8 +353,177 @@ Documentation Generation ------------------------ =20 The framework exports specifications via debugfs that can be used -to generate documentation. Tools for automatic documentation generation -from specifications are planned for future development. +to generate documentation. The ``kapi`` tool provides comprehensive +extraction and formatting capabilities for kernel API specifications. + +The kapi Tool +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +Overview +-------- + +The ``kapi`` tool is a userspace utility that extracts and displays kernel= API +specifications from multiple sources. It provides a unified interface to a= ccess +API documentation whether from compiled kernels, source code, or runtime s= ystems. + +Installation +------------ + +Build the tool from the kernel source tree:: + + $ cd tools/kapi + $ cargo build --release + + # Optional: Install system-wide + $ cargo install --path . + +The tool requires Rust and Cargo to build. The binary will be available at +``tools/kapi/target/release/kapi``. + +Command-Line Usage +------------------ + +Basic syntax:: + + kapi [OPTIONS] [API_NAME] + +Options: + +- ``--vmlinux ``: Extract from compiled kernel binary +- ``--source ``: Extract from kernel source code +- ``--debugfs ``: Extract from debugfs (default: /sys/kernel/debug) +- ``-f, --format ``: Output format (plain, json, rst) +- ``-h, --help``: Display help information +- ``-V, --version``: Display version information + +Input Modes +----------- + +**1. Source Code Mode** + +Extract specifications directly from kernel source:: + + # Scan entire kernel source tree + $ kapi --source /path/to/linux + + # Extract from specific file + $ kapi --source kernel/sched/core.c + + # Get details for specific API + $ kapi --source /path/to/linux sys_sched_yield + +**2. Vmlinux Mode** + +Extract from compiled kernel with debug symbols:: + + # List all APIs in vmlinux + $ kapi --vmlinux /boot/vmlinux-5.15.0 + + # Get specific syscall details + $ kapi --vmlinux ./vmlinux sys_read + +**3. Debugfs Mode** + +Extract from running kernel via debugfs:: + + # Use default debugfs path + $ kapi + + # Use custom debugfs mount + $ kapi --debugfs /mnt/debugfs + + # Get specific API from running kernel + $ kapi sys_write + +Output Formats +-------------- + +**Plain Text Format** (default):: + + $ kapi sys_read + + Detailed information for sys_read: + =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + Description: Read from a file descriptor + + Detailed Description: + Reads up to count bytes from file descriptor fd into the buffer starti= ng at buf. + + Execution Context: + - KAPI_CTX_PROCESS | KAPI_CTX_SLEEPABLE + + Parameters (3): + + Available since: 1.0 + +**JSON Format**:: + + $ kapi --format json sys_read + { + "api_details": { + "name": "sys_read", + "description": "Read from a file descriptor", + "long_description": "Reads up to count bytes...", + "context_flags": ["KAPI_CTX_PROCESS | KAPI_CTX_SLEEPABLE"], + "since_version": "1.0" + } + } + +**ReStructuredText Format**:: + + $ kapi --format rst sys_read + + sys_read + =3D=3D=3D=3D=3D=3D=3D=3D + + **Read from a file descriptor** + + Reads up to count bytes from file descriptor fd into the buffer... + +Usage Examples +-------------- + +**Generate complete API documentation**:: + + # Export all kernel APIs to JSON + $ kapi --source /path/to/linux --format json > kernel-apis.json + + # Generate RST documentation for all syscalls + $ kapi --vmlinux ./vmlinux --format rst > syscalls.rst + + # List APIs from specific subsystem + $ kapi --source drivers/gpu/drm/ + +**Integration with other tools**:: + + # Find all APIs that can sleep + $ kapi --format json | jq '.apis[] | select(.context_flags[] | contain= s("SLEEPABLE"))' + + # Generate markdown documentation + $ kapi --format rst sys_mmap | pandoc -f rst -t markdown + +**Debugging and analysis**:: + + # Compare API between kernel versions + $ diff <(kapi --vmlinux vmlinux-5.10) <(kapi --vmlinux vmlinux-5.15) + + # Check if specific API exists + $ kapi --source . my_custom_api || echo "API not found" + +Implementation Details +---------------------- + +The tool extracts API specifications from three sources: + +1. **Source Code**: Parses KAPI specification macros using regular express= ions +2. **Vmlinux**: Reads the ``.kapi_specs`` ELF section from compiled kernels +3. **Debugfs**: Reads from ``/sys/kernel/debug/kapi/`` filesystem interface + +The tool supports all KAPI specification types: + +- System calls (``DEFINE_KERNEL_API_SPEC``) +- IOCTLs (``DEFINE_IOCTL_API_SPEC``) +- Kernel functions (``KAPI_DEFINE_SPEC``) =20 IDE Integration --------------- @@ -357,6 +535,11 @@ Modern IDEs can use the JSON export for: - Context validation - Error code documentation =20 +Example IDE integration:: + + # Generate IDE completion data + $ kapi --format json > .vscode/kernel-apis.json + Testing Framework ----------------- =20 @@ -367,6 +550,15 @@ The framework includes test helpers:: kapi_test_api("kmalloc", test_cases); #endif =20 +The kapi tool can verify specifications against implementations:: + + # Run consistency tests + $ cd tools/kapi + $ ./test_consistency.sh + + # Compare source vs binary specifications + $ ./compare_all_syscalls.sh + Best Practices =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D =20 diff --git a/tools/kapi/.gitignore b/tools/kapi/.gitignore new file mode 100644 index 0000000000000..1390bfc12686c --- /dev/null +++ b/tools/kapi/.gitignore @@ -0,0 +1,4 @@ +# Rust build artifacts +/target/ +**/*.rs.bk + diff --git a/tools/kapi/Cargo.toml b/tools/kapi/Cargo.toml new file mode 100644 index 0000000000000..4e6bcb10d132f --- /dev/null +++ b/tools/kapi/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name =3D "kapi" +version =3D "0.1.0" +edition =3D "2024" +authors =3D ["Sasha Levin "] +description =3D "Tool for extracting and displaying kernel API specificati= ons" +license =3D "GPL-2.0" + +[dependencies] +goblin =3D "0.10" +clap =3D { version =3D "4.4", features =3D ["derive"] } +anyhow =3D "1.0" +serde =3D { version =3D "1.0", features =3D ["derive"] } +serde_json =3D "1.0" +regex =3D "1.10" +walkdir =3D "2.4" + +[dev-dependencies] +tempfile =3D "3.8" diff --git a/tools/kapi/src/extractor/debugfs.rs b/tools/kapi/src/extractor= /debugfs.rs new file mode 100644 index 0000000000000..91775dea223f5 --- /dev/null +++ b/tools/kapi/src/extractor/debugfs.rs @@ -0,0 +1,204 @@ +use anyhow::{Context, Result, bail}; +use std::fs; +use std::io::Write; +use std::path::PathBuf; +use crate::formatter::OutputFormatter; + +use super::{ApiExtractor, ApiSpec, display_api_spec}; + +/// Extractor for kernel API specifications from debugfs +pub struct DebugfsExtractor { + debugfs_path: PathBuf, +} + +impl DebugfsExtractor { + /// Create a new debugfs extractor with the specified debugfs path + pub fn new(debugfs_path: Option) -> Result { + let path =3D match debugfs_path { + Some(p) =3D> PathBuf::from(p), + None =3D> PathBuf::from("/sys/kernel/debug"), + }; + + // Check if the debugfs path exists + if !path.exists() { + bail!("Debugfs path does not exist: {}", path.display()); + } + + // Check if kapi directory exists + let kapi_path =3D path.join("kapi"); + if !kapi_path.exists() { + bail!("Kernel API debugfs interface not found at: {}", kapi_pa= th.display()); + } + + Ok(Self { + debugfs_path: path, + }) + } + + /// Parse the list file to get all available API names + fn parse_list_file(&self) -> Result> { + let list_path =3D self.debugfs_path.join("kapi/list"); + let content =3D fs::read_to_string(&list_path) + .with_context(|| format!("Failed to read {}", list_path.displa= y()))?; + + let mut apis =3D Vec::new(); + let mut in_list =3D false; + + for line in content.lines() { + if line.contains("=3D=3D=3D") { + in_list =3D true; + continue; + } + + if in_list && line.starts_with("Total:") { + break; + } + + if in_list && !line.trim().is_empty() { + // Extract API name from lines like "sys_read - Read from = a file descriptor" + if let Some(name) =3D line.split(" - ").next() { + apis.push(name.trim().to_string()); + } + } + } + + Ok(apis) + } + + /// Parse a single API specification file + fn parse_spec_file(&self, api_name: &str) -> Result { + let spec_path =3D self.debugfs_path.join(format!("kapi/specs/{}", = api_name)); + let content =3D fs::read_to_string(&spec_path) + .with_context(|| format!("Failed to read {}", spec_path.displa= y()))?; + + let mut spec =3D ApiSpec { + name: api_name.to_string(), + api_type: "unknown".to_string(), + description: None, + long_description: None, + version: None, + context_flags: Vec::new(), + param_count: None, + error_count: None, + examples: None, + notes: None, + since_version: None, + }; + + // Parse the content + let mut collecting_multiline =3D false; + let mut multiline_buffer =3D String::new(); + let mut multiline_field =3D ""; + + for line in content.lines() { + // Handle section headers + if line.starts_with("Parameters (") { + if let Some(count_str) =3D line.strip_prefix("Parameters (= ").and_then(|s| s.strip_suffix("):")) { + spec.param_count =3D count_str.parse().ok(); + } + continue; + } else if line.starts_with("Errors (") { + if let Some(count_str) =3D line.strip_prefix("Errors (").a= nd_then(|s| s.strip_suffix("):")) { + spec.error_count =3D count_str.parse().ok(); + } + continue; + } else if line.starts_with("Examples:") { + collecting_multiline =3D true; + multiline_field =3D "examples"; + multiline_buffer.clear(); + continue; + } else if line.starts_with("Notes:") { + collecting_multiline =3D true; + multiline_field =3D "notes"; + multiline_buffer.clear(); + continue; + } + + // Handle multiline sections + if collecting_multiline { + if line.trim().is_empty() && multiline_buffer.ends_with("\= n\n") { + collecting_multiline =3D false; + match multiline_field { + "examples" =3D> spec.examples =3D Some(multiline_b= uffer.trim().to_string()), + "notes" =3D> spec.notes =3D Some(multiline_buffer.= trim().to_string()), + _ =3D> {} + } + multiline_buffer.clear(); + } else { + if !multiline_buffer.is_empty() { + multiline_buffer.push('\n'); + } + multiline_buffer.push_str(line); + } + continue; + } + + // Parse regular fields + if let Some(desc) =3D line.strip_prefix("Description: ") { + spec.description =3D Some(desc.to_string()); + } else if let Some(long_desc) =3D line.strip_prefix("Long desc= ription: ") { + spec.long_description =3D Some(long_desc.to_string()); + } else if let Some(version) =3D line.strip_prefix("Version: ")= { + spec.version =3D Some(version.to_string()); + } else if let Some(since) =3D line.strip_prefix("Since: ") { + spec.since_version =3D Some(since.to_string()); + } else if let Some(flags) =3D line.strip_prefix("Context flags= : ") { + spec.context_flags =3D flags.split_whitespace() + .map(|s| s.to_string()) + .collect(); + } + } + + // Determine API type based on name + if api_name.starts_with("sys_") { + spec.api_type =3D "syscall".to_string(); + } else if api_name.contains("_ioctl") || api_name.starts_with("ioc= tl_") { + spec.api_type =3D "ioctl".to_string(); + } else { + spec.api_type =3D "function".to_string(); + } + + Ok(spec) + } +} + +impl ApiExtractor for DebugfsExtractor { + fn extract_all(&self) -> Result> { + let api_names =3D self.parse_list_file()?; + let mut specs =3D Vec::new(); + + for name in api_names { + match self.parse_spec_file(&name) { + Ok(spec) =3D> specs.push(spec), + Err(e) =3D> eprintln!("Warning: Failed to parse spec for {= }: {}", name, e), + } + } + + Ok(specs) + } + + fn extract_by_name(&self, name: &str) -> Result> { + let api_names =3D self.parse_list_file()?; + + if api_names.contains(&name.to_string()) { + Ok(Some(self.parse_spec_file(name)?)) + } else { + Ok(None) + } + } + + fn display_api_details( + &self, + api_name: &str, + formatter: &mut dyn OutputFormatter, + writer: &mut dyn Write, + ) -> Result<()> { + if let Some(spec) =3D self.extract_by_name(api_name)? { + display_api_spec(&spec, formatter, writer)?; + } else { + writeln!(writer, "API '{}' not found in debugfs", api_name)?; + } + + Ok(()) + } +} \ No newline at end of file diff --git a/tools/kapi/src/extractor/mod.rs b/tools/kapi/src/extractor/mod= .rs new file mode 100644 index 0000000000000..bc55201152e3e --- /dev/null +++ b/tools/kapi/src/extractor/mod.rs @@ -0,0 +1,95 @@ +use anyhow::Result; +use std::io::Write; +use crate::formatter::OutputFormatter; + +pub mod vmlinux; +pub mod source_parser; +pub mod debugfs; + +pub use vmlinux::VmlinuxExtractor; +pub use source_parser::SourceExtractor; +pub use debugfs::DebugfsExtractor; + +/// Common API specification information that all extractors should provide +#[derive(Debug, Clone)] +pub struct ApiSpec { + pub name: String, + pub api_type: String, + pub description: Option, + pub long_description: Option, + pub version: Option, + pub context_flags: Vec, + pub param_count: Option, + pub error_count: Option, + pub examples: Option, + pub notes: Option, + pub since_version: Option, +} + +/// Trait for extracting API specifications from different sources +pub trait ApiExtractor { + /// Extract all API specifications from the source + fn extract_all(&self) -> Result>; + + /// Extract a specific API specification by name + fn extract_by_name(&self, name: &str) -> Result>; + + /// Display detailed information about a specific API + fn display_api_details( + &self, + api_name: &str, + formatter: &mut dyn OutputFormatter, + writer: &mut dyn Write, + ) -> Result<()>; +} + +/// Helper function to display an ApiSpec using a formatter +pub fn display_api_spec( + spec: &ApiSpec, + formatter: &mut dyn OutputFormatter, + writer: &mut dyn Write, +) -> Result<()> { + formatter.begin_api_details(writer, &spec.name)?; + + if let Some(desc) =3D &spec.description { + formatter.description(writer, desc)?; + } + + if let Some(long_desc) =3D &spec.long_description { + formatter.long_description(writer, long_desc)?; + } + + if let Some(version) =3D &spec.since_version { + formatter.since_version(writer, version)?; + } + + if !spec.context_flags.is_empty() { + formatter.begin_context_flags(writer)?; + for flag in &spec.context_flags { + formatter.context_flag(writer, flag)?; + } + formatter.end_context_flags(writer)?; + } + + if let Some(param_count) =3D spec.param_count { + formatter.begin_parameters(writer, param_count)?; + formatter.end_parameters(writer)?; + } + + if let Some(error_count) =3D spec.error_count { + formatter.begin_errors(writer, error_count)?; + formatter.end_errors(writer)?; + } + + if let Some(notes) =3D &spec.notes { + formatter.notes(writer, notes)?; + } + + if let Some(examples) =3D &spec.examples { + formatter.examples(writer, examples)?; + } + + formatter.end_api_details(writer)?; + + Ok(()) +} \ No newline at end of file diff --git a/tools/kapi/src/extractor/source_parser.rs b/tools/kapi/src/ext= ractor/source_parser.rs new file mode 100644 index 0000000000000..8de35f5a73916 --- /dev/null +++ b/tools/kapi/src/extractor/source_parser.rs @@ -0,0 +1,488 @@ +use anyhow::{Context, Result}; +use regex::Regex; +use std::fs; +use std::path::Path; +use std::collections::HashMap; +use walkdir::WalkDir; +use std::io::Write; +use crate::formatter::OutputFormatter; +use super::{ApiExtractor, ApiSpec, display_api_spec}; + +#[derive(Debug, Clone)] +pub struct SourceApiSpec { + pub name: String, + pub api_type: ApiType, + pub parsed_fields: HashMap, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ApiType { + Syscall, + Ioctl, + Function, + Unknown, +} + +impl ApiType { + fn from_name(name: &str) -> Self { + if name.starts_with("sys_") { + ApiType::Syscall + } else if name.contains("ioctl") || name.contains("IOCTL") { + ApiType::Ioctl + } else if name.starts_with("do_") || name.starts_with("__") { + ApiType::Function + } else { + ApiType::Unknown + } + } +} + +pub struct SourceParser { + // Regex patterns for matching KAPI specifications + spec_start_pattern: Regex, + spec_end_pattern: Regex, + ioctl_spec_pattern: Regex, +} + +impl SourceParser { + pub fn new() -> Result { + Ok(SourceParser { + // Match DEFINE_KERNEL_API_SPEC(function_name) + spec_start_pattern: Regex::new(r"DEFINE_KERNEL_API_SPEC\s*\(\s= *([a-zA-Z_][a-zA-Z0-9_]*)\s*\)")?, + // Match KAPI_END_SPEC + spec_end_pattern: Regex::new(r"KAPI_END_SPEC")?, + // Match IOCTL specifications + ioctl_spec_pattern: Regex::new(r#"DEFINE_IOCTL_API_SPEC\s*\(\s= *([a-zA-Z_][a-zA-Z0-9_]*)\s*,\s*([^,]+)\s*,\s*"([^"]+)"\s*\)"#)?, + }) + } + + /// Parse a single source file for KAPI specifications + pub fn parse_file(&self, path: &Path) -> Result> { + let content =3D fs::read_to_string(path) + .with_context(|| format!("Failed to read file: {}", path.displ= ay()))?; + + self.parse_content(&content, path) + } + + /// Parse file content for KAPI specifications + pub fn parse_content(&self, content: &str, _file_path: &Path) -> Resul= t> { + let mut specs =3D Vec::new(); + let lines: Vec<&str> =3D content.lines().collect(); + + // First, look for standard KAPI specs + for (i, line) in lines.iter().enumerate() { + if let Some(captures) =3D self.spec_start_pattern.captures(lin= e) { + let api_name =3D captures.get(1).unwrap().as_str().to_stri= ng(); + + // Find the end of this specification + if let Some(spec_content) =3D self.extract_spec_block(&lin= es, i) { + let mut spec =3D SourceApiSpec { + name: api_name.clone(), + api_type: ApiType::from_name(&api_name), + parsed_fields: HashMap::new(), + }; + + // Parse the fields + self.parse_spec_fields(&spec_content, &mut spec.parsed= _fields)?; + + specs.push(spec); + } + } + + // Also look for IOCTL specs + if let Some(captures) =3D self.ioctl_spec_pattern.captures(lin= e) { + let spec_name =3D captures.get(1).unwrap().as_str().to_str= ing(); + let cmd =3D captures.get(2).unwrap().as_str().to_string(); + let cmd_name =3D captures.get(3).unwrap().as_str().to_stri= ng(); + + // Find the end of this IOCTL specification + if let Some(spec_content) =3D self.extract_ioctl_spec_bloc= k(&lines, i) { + let mut spec =3D SourceApiSpec { + name: spec_name, + api_type: ApiType::Ioctl, + parsed_fields: HashMap::new(), + }; + + // Add IOCTL-specific fields + spec.parsed_fields.insert("cmd".to_string(), cmd); + spec.parsed_fields.insert("cmd_name".to_string(), cmd_= name); + + // Parse other fields + self.parse_spec_fields(&spec_content, &mut spec.parsed= _fields)?; + + specs.push(spec); + } + } + } + + Ok(specs) + } + + /// Extract a complete KAPI specification block from the source + fn extract_spec_block(&self, lines: &[&str], start_idx: usize) -> Opti= on { + let mut spec_lines =3D Vec::new(); + let mut brace_count =3D 0; + let mut in_spec =3D false; + + for (_i, line) in lines.iter().enumerate().skip(start_idx) { + spec_lines.push(line.to_string()); + + // Count braces to handle nested structures + for ch in line.chars() { + match ch { + '{' =3D> { + brace_count +=3D 1; + in_spec =3D true; + } + '}' =3D> { + brace_count -=3D 1; + } + _ =3D> {} + } + } + + // Check for end of spec + if self.spec_end_pattern.is_match(line) { + return Some(spec_lines.join("\n")); + } + + // Alternative end: closing brace with semicolon + if in_spec && brace_count =3D=3D 0 && line.contains("};") { + return Some(spec_lines.join("\n")); + } + } + + None + } + + /// Extract a complete IOCTL specification block + fn extract_ioctl_spec_block(&self, lines: &[&str], start_idx: usize) -= > Option { + let mut spec_lines =3D Vec::new(); + let mut brace_count =3D 0; + + for (i, line) in lines.iter().enumerate().skip(start_idx) { + spec_lines.push(line.to_string()); + + // Count braces + for ch in line.chars() { + match ch { + '{' =3D> brace_count +=3D 1, + '}' =3D> brace_count -=3D 1, + _ =3D> {} + } + } + + // Check for end patterns + if line.contains("KAPI_END_IOCTL_SPEC") || line.contains("KAPI= _IOCTL_END_SPEC") { + return Some(spec_lines.join("\n")); + } + + // Alternative end: closing brace with semicolon at top level + if brace_count =3D=3D 0 && line.contains("};") && i > start_id= x { + return Some(spec_lines.join("\n")); + } + } + + None + } + + /// Parse individual KAPI fields from the specification + fn parse_spec_fields(&self, content: &str, fields: &mut HashMap) -> Result<()> { + // Parse KAPI_DESCRIPTION + if let Some(captures) =3D Regex::new(r#"KAPI_DESCRIPTION\s*\(\s*"(= [^"]*)"\s*\)"#)?.captures(content) { + fields.insert("description".to_string(), captures.get(1).unwra= p().as_str().to_string()); + } + + // Parse KAPI_LONG_DESC (handle multi-line) + if let Some(captures) =3D Regex::new(r#"KAPI_LONG_DESC\s*\(\s*"([^= "]*(?:\s*"[^"]*)*?)"\s*\)"#)?.captures(content) { + let long_desc =3D captures.get(1).unwrap().as_str() + .replace("\"\n\t\t \"", " ") + .replace("\"\n\t\t \"", " ") + .replace("\"\n\t\t \"", " ") + .replace("\"\n\t\t \"", " ") + .replace("\"\n\t\t \"", " ") + .replace("\"\n\t\t\"", " "); + fields.insert("long_description".to_string(), long_desc); + } + + // Parse KAPI_CONTEXT + if let Some(captures) =3D Regex::new(r"KAPI_CONTEXT\s*\(([^)]+)\)"= )?.captures(content) { + fields.insert("context".to_string(), captures.get(1).unwrap().= as_str().to_string()); + } + + // Parse KAPI_NOTES (handle multi-line) + if let Some(captures) =3D Regex::new(r#"KAPI_NOTES\s*\(\s*"([^"]*(= ?:\s*"[^"]*)*?)"\s*\)"#)?.captures(content) { + let notes =3D captures.get(1).unwrap().as_str() + .replace("\"\n\t\t \"", " ") + .replace("\"\n\t\t \"", " ") + .replace("\"\n\t\t \"", " ") + .replace("\"\n\t\t \"", " ") + .replace("\"\n\t\t \"", " ") + .replace("\"\n\t\t\"", " ") + .trim() + .to_string(); + fields.insert("notes".to_string(), notes); + } + + // Parse KAPI_EXAMPLES (handle multi-line) + if let Some(captures) =3D Regex::new(r#"KAPI_EXAMPLES\s*\(\s*"([^"= ]*(?:\s*"[^"]*)*?)"\s*\)"#)?.captures(content) { + let examples =3D captures.get(1).unwrap().as_str() + .replace("\\n\"\n\t\t \"", "\n") + .replace("\\n\"\n\t\t \"", "\n") + .replace("\\n\"\n\t\t \"", "\n") + .replace("\\n\"\n\t\t \"", "\n") + .replace("\\n\"\n\t\t\"", "\n") + .replace("\\n", "\n") + .trim() + .to_string(); + fields.insert("examples".to_string(), examples); + } + + // Parse KAPI_SINCE_VERSION + if let Some(captures) =3D Regex::new(r#"KAPI_SINCE_VERSION\s*\(\s*= "([^"]*)"\s*\)"#)?.captures(content) { + fields.insert("since_version".to_string(), captures.get(1).unw= rap().as_str().to_string()); + } + + // Parse parameter count + let param_regex =3D Regex::new(r"KAPI_PARAM\s*\(\s*(\d+)\s*,")?; + let mut max_param_idx =3D 0; + for captures in param_regex.captures_iter(content) { + if let Ok(idx) =3D captures.get(1).unwrap().as_str().parse::() { + max_param_idx =3D max_param_idx.max(idx + 1); + } + } + if max_param_idx > 0 { + fields.insert("param_count".to_string(), max_param_idx.to_stri= ng()); + } + + // Parse error count + let error_regex =3D Regex::new(r"KAPI_ERROR\s*\(\s*(\d+)\s*,")?; + let mut max_error_idx =3D 0; + for captures in error_regex.captures_iter(content) { + if let Ok(idx) =3D captures.get(1).unwrap().as_str().parse::() { + max_error_idx =3D max_error_idx.max(idx + 1); + } + } + if max_error_idx > 0 { + fields.insert("error_count".to_string(), max_error_idx.to_stri= ng()); + } + + // Parse other counts + if content.contains(".error_count =3D") { + if let Some(captures) =3D Regex::new(r"\.error_count\s*=3D\s*(= \d+)")?.captures(content) { + fields.insert("error_count".to_string(), captures.get(1).u= nwrap().as_str().to_string()); + } + } + + if content.contains(".param_count =3D") { + if let Some(captures) =3D Regex::new(r"\.param_count\s*=3D\s*(= \d+)")?.captures(content) { + fields.insert("param_count".to_string(), captures.get(1).u= nwrap().as_str().to_string()); + } + } + + // Parse .since_version + if let Some(captures) =3D Regex::new(r#"\.since_version\s*=3D\s*"(= [^"]*)""#)?.captures(content) { + fields.insert("since_version".to_string(), captures.get(1).unw= rap().as_str().to_string()); + } + + // Parse .notes (handle multi-line) + if let Some(captures) =3D Regex::new(r#"\.notes\s*=3D\s*"([^"]*(?:= \s*"[^"]*)*?)""#)?.captures(content) { + let notes =3D captures.get(1).unwrap().as_str() + .replace("\"\n\t\t \"", " ") + .replace("\"\n\t\t\"", " ") + .replace("\"\n\t \"", " ") // Handle single tab + space + .trim() + .to_string(); + fields.insert("notes".to_string(), notes); + } + + // Parse .examples (handle multi-line) + if let Some(captures) =3D Regex::new(r#"\.examples\s*=3D\s*"([^"]*= (?:\s*"[^"]*)*?)""#)?.captures(content) { + let examples =3D captures.get(1).unwrap().as_str() + .replace("\\n\"\n\t\t \"", "\n") + .replace("\\n", "\n"); + fields.insert("examples".to_string(), examples); + } + + Ok(()) + } + + /// Scan a directory tree for files containing KAPI specifications + pub fn scan_directory(&self, dir: &Path, extensions: &[&str]) -> Resul= t> { + let mut all_specs =3D Vec::new(); + + for entry in WalkDir::new(dir) + .follow_links(true) + .into_iter() + .filter_map(|e| e.ok()) + { + let path =3D entry.path(); + + // Skip non-files + if !path.is_file() { + continue; + } + + // Check file extension + if let Some(ext) =3D path.extension() { + if extensions.iter().any(|&e| ext =3D=3D e) { + // Try to parse the file + match self.parse_file(path) { + Ok(specs) =3D> { + if !specs.is_empty() { + all_specs.extend(specs); + } + } + Err(e) =3D> { + eprintln!("Warning: Failed to parse {}: {}", p= ath.display(), e); + } + } + } + } + } + + Ok(all_specs) + } + +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::NamedTempFile; + + #[test] + fn test_parse_syscall_spec() { + let parser =3D SourceParser::new().unwrap(); + + let content =3D r#" +DEFINE_KERNEL_API_SPEC(sys_mlock) + KAPI_DESCRIPTION("Lock pages in memory") + KAPI_LONG_DESC("Locks pages in the specified address range into RAM") + KAPI_CONTEXT(KAPI_CTX_PROCESS | KAPI_CTX_SLEEPABLE) + + KAPI_PARAM(0, "start", "unsigned long", "Starting address") + KAPI_PARAM_END + + KAPI_PARAM(1, "len", "size_t", "Length of range") + KAPI_PARAM_END + + .param_count =3D 2, + .error_count =3D 3, + +KAPI_END_SPEC +"#; + + let mut temp_file =3D NamedTempFile::new().unwrap(); + write!(temp_file, "{}", content).unwrap(); + + let specs =3D parser.parse_content(content, temp_file.path()).unwr= ap(); + + assert_eq!(specs.len(), 1); + assert_eq!(specs[0].name, "sys_mlock"); + assert_eq!(specs[0].api_type, ApiType::Syscall); + assert_eq!(specs[0].parsed_fields.get("description").unwrap(), "Lo= ck pages in memory"); + assert_eq!(specs[0].parsed_fields.get("param_count").unwrap(), "2"= ); + } + + #[test] + fn test_parse_ioctl_spec() { + let parser =3D SourceParser::new().unwrap(); + + let content =3D r#" +DEFINE_IOCTL_API_SPEC(binder_write_read, BINDER_WRITE_READ, "BINDER_WRITE_= READ") + KAPI_DESCRIPTION("Perform read/write operations on binder") + KAPI_CONTEXT(KAPI_CTX_PROCESS | KAPI_CTX_SLEEPABLE) + + KAPI_PARAM(0, "write_size", "binder_size_t", "Bytes to write") + KAPI_PARAM_END + +KAPI_END_IOCTL_SPEC +"#; + + let mut temp_file =3D NamedTempFile::new().unwrap(); + write!(temp_file, "{}", content).unwrap(); + + let specs =3D parser.parse_content(content, temp_file.path()).unwr= ap(); + + assert_eq!(specs.len(), 1); + assert_eq!(specs[0].name, "binder_write_read"); + assert_eq!(specs[0].api_type, ApiType::Ioctl); + assert_eq!(specs[0].parsed_fields.get("cmd_name").unwrap(), "BINDE= R_WRITE_READ"); + } +} + +// SourceExtractor implementation +pub struct SourceExtractor { + specs: Vec, +} + +impl SourceExtractor { + pub fn new(path: String) -> Result { + let parser =3D SourceParser::new()?; + let path_obj =3D Path::new(&path); + + let specs =3D if path_obj.is_file() { + parser.parse_file(path_obj)? + } else if path_obj.is_dir() { + parser.scan_directory(path_obj, &["c", "h"])? + } else { + anyhow::bail!("Path does not exist: {}", path_obj.display()) + }; + + Ok(SourceExtractor { specs }) + } + + fn convert_to_api_spec(&self, source_spec: &SourceApiSpec) -> ApiSpec { + ApiSpec { + name: source_spec.name.clone(), + api_type: match source_spec.api_type { + ApiType::Syscall =3D> "syscall".to_string(), + ApiType::Ioctl =3D> "ioctl".to_string(), + ApiType::Function =3D> "function".to_string(), + ApiType::Unknown =3D> "unknown".to_string(), + }, + description: source_spec.parsed_fields.get("description").clon= ed(), + long_description: source_spec.parsed_fields.get("long_descript= ion").cloned(), + version: source_spec.parsed_fields.get("version").cloned(), + context_flags: source_spec.parsed_fields.get("context") + .map(|c| vec![c.clone()]) + .unwrap_or_default(), + param_count: source_spec.parsed_fields.get("param_count") + .and_then(|s| s.parse::().ok()), + error_count: source_spec.parsed_fields.get("error_count") + .and_then(|s| s.parse::().ok()), + examples: source_spec.parsed_fields.get("examples").cloned(), + notes: source_spec.parsed_fields.get("notes").cloned(), + since_version: source_spec.parsed_fields.get("since_version").= cloned(), + } + } +} + +impl ApiExtractor for SourceExtractor { + fn extract_all(&self) -> Result> { + Ok(self.specs.iter() + .map(|s| self.convert_to_api_spec(s)) + .collect()) + } + + fn extract_by_name(&self, name: &str) -> Result> { + Ok(self.specs.iter() + .find(|s| s.name =3D=3D name) + .map(|s| self.convert_to_api_spec(s))) + } + + fn display_api_details( + &self, + api_name: &str, + formatter: &mut dyn OutputFormatter, + writer: &mut dyn Write, + ) -> Result<()> { + if let Some(spec) =3D self.specs.iter().find(|s| s.name =3D=3D api= _name) { + let api_spec =3D self.convert_to_api_spec(spec); + display_api_spec(&api_spec, formatter, writer)?; + } + Ok(()) + } +} \ No newline at end of file diff --git a/tools/kapi/src/extractor/vmlinux/binary_utils.rs b/tools/kapi/= src/extractor/vmlinux/binary_utils.rs new file mode 100644 index 0000000000000..02c8e3b8eda77 --- /dev/null +++ b/tools/kapi/src/extractor/vmlinux/binary_utils.rs @@ -0,0 +1,130 @@ +use anyhow::Result; +use std::io::Write; +use crate::formatter::OutputFormatter; + +// Constants for all structure field sizes +pub mod sizes { + pub const NAME: usize =3D 128; + pub const DESC: usize =3D 512; + pub const MAX_PARAMS: usize =3D 16; + pub const MAX_ERRORS: usize =3D 32; + pub const MAX_CONSTRAINTS: usize =3D 16; +} + +// Helper for reading data at specific offsets +pub struct DataReader<'a> { + data: &'a [u8], + pos: usize, +} + +impl<'a> DataReader<'a> { + pub fn new(data: &'a [u8], offset: usize) -> Self { + Self { data, pos: offset } + } + + pub fn read_bytes(&mut self, len: usize) -> Option<&'a [u8]> { + if self.pos + len <=3D self.data.len() { + let bytes =3D &self.data[self.pos..self.pos + len]; + self.pos +=3D len; + Some(bytes) + } else { + None + } + } + + pub fn read_cstring(&mut self, max_len: usize) -> Option { + let bytes =3D self.read_bytes(max_len)?; + if let Some(null_pos) =3D bytes.iter().position(|&b| b =3D=3D 0) { + if null_pos > 0 { + if let Ok(s) =3D std::str::from_utf8(&bytes[..null_pos]) { + return Some(s.to_string()); + } + } + } + None + } + + pub fn read_u32(&mut self) -> Option { + let bytes =3D self.read_bytes(4)?; + Some(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])) + } + + pub fn skip(&mut self, len: usize) { + self.pos =3D (self.pos + len).min(self.data.len()); + } +} + +#[allow(dead_code)] +pub fn parse_context_flags(flags: u32, formatter: &mut dyn OutputFormatter= , w: &mut dyn Write) -> Result<()> { + // Context flags from kernel headers + const KAPI_CTX_PROCESS: u32 =3D 1 << 0; + const KAPI_CTX_SOFTIRQ: u32 =3D 1 << 1; + const KAPI_CTX_HARDIRQ: u32 =3D 1 << 2; + const KAPI_CTX_NMI: u32 =3D 1 << 3; + const KAPI_CTX_USER: u32 =3D 1 << 4; + const KAPI_CTX_KERNEL: u32 =3D 1 << 5; + const KAPI_CTX_SLEEPABLE: u32 =3D 1 << 6; + const KAPI_CTX_ATOMIC: u32 =3D 1 << 7; + const KAPI_CTX_PREEMPTIBLE: u32 =3D 1 << 8; + const KAPI_CTX_MIGRATION_DISABLED: u32 =3D 1 << 9; + + if flags & KAPI_CTX_PROCESS !=3D 0 { formatter.context_flag(w, "Proces= s context")?; } + if flags & KAPI_CTX_SOFTIRQ !=3D 0 { formatter.context_flag(w, "Softir= q context")?; } + if flags & KAPI_CTX_HARDIRQ !=3D 0 { formatter.context_flag(w, "Hardir= q context")?; } + if flags & KAPI_CTX_NMI !=3D 0 { formatter.context_flag(w, "NMI contex= t")?; } + if flags & KAPI_CTX_USER !=3D 0 { formatter.context_flag(w, "User mode= ")?; } + if flags & KAPI_CTX_KERNEL !=3D 0 { formatter.context_flag(w, "Kernel = mode")?; } + if flags & KAPI_CTX_SLEEPABLE !=3D 0 { formatter.context_flag(w, "May = sleep")?; } + if flags & KAPI_CTX_ATOMIC !=3D 0 { formatter.context_flag(w, "Atomic = context")?; } + if flags & KAPI_CTX_PREEMPTIBLE !=3D 0 { formatter.context_flag(w, "Pr= eemptible")?; } + if flags & KAPI_CTX_MIGRATION_DISABLED !=3D 0 { formatter.context_flag= (w, "Migration disabled")?; } + + Ok(()) +} + +// Structure layout definitions for calculating sizes +pub fn param_spec_layout_size() -> usize { + // Packed structure + sizes::NAME * 2 + // name, type_name + 4 + 4 + // type, flags + 8 + 8 + // size, alignment + 8 + 8 + // min_value, max_value + 8 + // valid_mask + 8 + // enum_values pointer + 4 + 4 + // enum_count, constraint_type + 8 + // validate pointer + sizes::DESC * 2 + // description, constraints + 4 + 8 // size_param_idx, size_multiplier +} + +pub fn return_spec_layout_size() -> usize { + // Packed structure + sizes::NAME + // type_name + 4 + 4 + // type, check_type + 8 + 8 + 8 + // success_value, success_min, success_max + 8 + // error_values pointer + 4 + // error_count + 8 + // is_success pointer + sizes::DESC // description +} + +pub fn error_spec_layout_size() -> usize { + // Packed structure + 4 + // code + sizes::NAME + // name + sizes::DESC * 2 // condition, description +} + +pub fn lock_spec_layout_size() -> usize { + // Packed structure + sizes::NAME + // name + 4 + // lock_type + 1 + 1 + 1 + 1 + // bools + sizes::DESC // description +} + +pub fn constraint_spec_layout_size() -> usize { + // Packed structure + sizes::NAME + // name + sizes::DESC * 2 // description, expression +} \ No newline at end of file diff --git a/tools/kapi/src/extractor/vmlinux/mod.rs b/tools/kapi/src/extra= ctor/vmlinux/mod.rs new file mode 100644 index 0000000000000..5d5ca413d77a2 --- /dev/null +++ b/tools/kapi/src/extractor/vmlinux/mod.rs @@ -0,0 +1,372 @@ +use anyhow::{Context, Result}; +use goblin::elf::Elf; +use std::fs; +use std::io::Write; +use crate::formatter::OutputFormatter; +use super::{ApiExtractor, ApiSpec}; + +mod binary_utils; +use binary_utils::{sizes, DataReader, + param_spec_layout_size, return_spec_layout_size, error_spec_layout_siz= e, + lock_spec_layout_size, constraint_spec_layout_size}; + +pub struct VmlinuxExtractor { + kapi_data: Vec, + specs: Vec, +} + +#[derive(Debug)] +struct KapiSpec { + name: String, + api_type: String, + offset: usize, +} + +impl VmlinuxExtractor { + pub fn new(vmlinux_path: String) -> Result { + let vmlinux_data =3D fs::read(&vmlinux_path) + .with_context(|| format!("Failed to read vmlinux file: {}", vm= linux_path))?; + + let elf =3D Elf::parse(&vmlinux_data) + .context("Failed to parse ELF file")?; + + // Find the .kapi_specs section + let kapi_section =3D elf.section_headers + .iter() + .find(|sh| { + if let Some(name) =3D elf.shdr_strtab.get_at(sh.sh_name) { + name =3D=3D ".kapi_specs" + } else { + false + } + }) + .context("Could not find .kapi_specs section in vmlinux")?; + + // Find __start_kapi_specs and __stop_kapi_specs symbols + let mut start_addr =3D None; + let mut stop_addr =3D None; + + for sym in &elf.syms { + if let Some(name) =3D elf.strtab.get_at(sym.st_name) { + match name { + "__start_kapi_specs" =3D> start_addr =3D Some(sym.st_v= alue), + "__stop_kapi_specs" =3D> stop_addr =3D Some(sym.st_val= ue), + _ =3D> {} + } + } + } + + let start =3D start_addr.context("Could not find __start_kapi_spec= s symbol")?; + let stop =3D stop_addr.context("Could not find __stop_kapi_specs s= ymbol")?; + + if stop <=3D start { + anyhow::bail!("No kernel API specifications found in vmlinux"); + } + + // Calculate the offset within the file + let section_vaddr =3D kapi_section.sh_addr; + let file_offset =3D kapi_section.sh_offset + (start - section_vadd= r); + let data_size =3D (stop - start) as usize; + + if file_offset as usize + data_size > vmlinux_data.len() { + anyhow::bail!("Invalid offset/size for .kapi_specs data"); + } + + // Extract the raw data + let kapi_data =3D vmlinux_data[file_offset as usize..(file_offset = as usize + data_size)].to_vec(); + + // Parse the specifications + let specs =3D parse_kapi_specs(&kapi_data)?; + + Ok(VmlinuxExtractor { + kapi_data, + specs, + }) + } + +} + +impl ApiExtractor for VmlinuxExtractor { + fn extract_all(&self) -> Result> { + // For vmlinux extractor, we return basic info only + // Detailed parsing happens in display_api_details + Ok(self.specs.iter().map(|spec| { + ApiSpec { + name: spec.name.clone(), + api_type: spec.api_type.clone(), + description: None, + long_description: None, + version: None, + context_flags: vec![], + param_count: None, + error_count: None, + examples: None, + notes: None, + since_version: None, + } + }).collect()) + } + + fn extract_by_name(&self, name: &str) -> Result> { + Ok(self.specs.iter() + .find(|s| s.name =3D=3D name) + .map(|spec| ApiSpec { + name: spec.name.clone(), + api_type: spec.api_type.clone(), + description: None, + long_description: None, + version: None, + context_flags: vec![], + param_count: None, + error_count: None, + examples: None, + notes: None, + since_version: None, + })) + } + + fn display_api_details( + &self, + api_name: &str, + formatter: &mut dyn OutputFormatter, + writer: &mut dyn Write, + ) -> Result<()> { + if let Some(spec) =3D self.specs.iter().find(|s| s.name =3D=3D api= _name) { + // Parse the binary data into an ApiSpec + let api_spec =3D parse_binary_to_api_spec(&self.kapi_data, spe= c.offset)?; + // Use the common display function + super::display_api_spec(&api_spec, formatter, writer)?; + } + Ok(()) + } +} + +fn parse_kapi_specs(data: &[u8]) -> Result> { + let mut specs =3D Vec::new(); + + // The kernel_api_spec struct size in the kernel is 308064 bytes + // This is calculated as sizeof(struct kernel_api_spec) which includes: + // - Basic fields (name, version, description, etc.) + // - Arrays for parameters, errors, locks, constraints + // - Additional metadata fields + // TODO: This should ideally be read from kernel headers or made confi= gurable + let struct_size =3D 308064; + + let mut offset =3D 0; + while offset + struct_size <=3D data.len() { + // Try to read the name at this offset + if let Some(name) =3D read_cstring(data, offset, 128) { + if is_valid_api_name(&name) { + let api_type =3D if name.starts_with("sys_") { + "syscall" + } else if name.contains("ioctl") || name.contains("IOCTL")= { + "ioctl" + } else { + "other" + }; + + specs.push(KapiSpec { + name: name.to_string(), + api_type: api_type.to_string(), + offset, + }); + } + } + + offset +=3D struct_size; + } + + // Handle any remaining data that might be a partial spec + if offset < data.len() && data.len() - offset >=3D 128 { + if let Some(name) =3D read_cstring(data, offset, 128) { + if is_valid_api_name(&name) { + let api_type =3D if name.starts_with("sys_") { + "syscall" + } else if name.contains("ioctl") || name.contains("IOCTL")= { + "ioctl" + } else { + "other" + }; + + specs.push(KapiSpec { + name: name.to_string(), + api_type: api_type.to_string(), + offset, + }); + } + } + } + + Ok(specs) +} + +fn read_cstring(data: &[u8], offset: usize, max_len: usize) -> Option { + if offset + max_len > data.len() { + return None; + } + + let bytes =3D &data[offset..offset + max_len]; + if let Some(null_pos) =3D bytes.iter().position(|&b| b =3D=3D 0) { + if null_pos > 0 { + if let Ok(s) =3D std::str::from_utf8(&bytes[..null_pos]) { + return Some(s.to_string()); + } + } + } + None +} + +fn is_valid_api_name(name: &str) -> bool { + if name.is_empty() || name.len() > 100 { + return false; + } + + name.chars().all(|c| c.is_ascii_alphanumeric() || c =3D=3D '_') + && (name.starts_with("sys_") + || name.contains("ioctl") + || name.contains("IOCTL") + || name.starts_with("do_") + || name.starts_with("__")) +} + +fn parse_binary_to_api_spec(data: &[u8], offset: usize) -> Result= { + let mut reader =3D DataReader::new(data, offset); + + // Read name + let name =3D reader.read_cstring(sizes::NAME) + .ok_or_else(|| anyhow::anyhow!("Failed to read API name"))?; + + // Read version + let version =3D reader.read_u32() + .map(|v| v.to_string()); + + // Read description + let description =3D reader.read_cstring(sizes::DESC) + .filter(|s| !s.is_empty()); + + // Read long description + let long_description =3D reader.read_cstring(sizes::DESC * 4) + .filter(|s| !s.is_empty()); + + // Read context flags + let context_flags =3D if let Some(flags) =3D reader.read_u32() { + let mut flag_strings =3D Vec::new(); + + const KAPI_CTX_PROCESS: u32 =3D 1 << 0; + const KAPI_CTX_SOFTIRQ: u32 =3D 1 << 1; + const KAPI_CTX_HARDIRQ: u32 =3D 1 << 2; + const KAPI_CTX_NMI: u32 =3D 1 << 3; + const KAPI_CTX_USER: u32 =3D 1 << 4; + const KAPI_CTX_KERNEL: u32 =3D 1 << 5; + const KAPI_CTX_SLEEPABLE: u32 =3D 1 << 6; + const KAPI_CTX_ATOMIC: u32 =3D 1 << 7; + const KAPI_CTX_PREEMPTIBLE: u32 =3D 1 << 8; + const KAPI_CTX_MIGRATION_DISABLED: u32 =3D 1 << 9; + + // Build the flag string similar to source format + let mut parts =3D Vec::new(); + if flags & KAPI_CTX_PROCESS !=3D 0 { parts.push("KAPI_CTX_PROCESS"= ); } + if flags & KAPI_CTX_SOFTIRQ !=3D 0 { parts.push("KAPI_CTX_SOFTIRQ"= ); } + if flags & KAPI_CTX_HARDIRQ !=3D 0 { parts.push("KAPI_CTX_HARDIRQ"= ); } + if flags & KAPI_CTX_NMI !=3D 0 { parts.push("KAPI_CTX_NMI"); } + if flags & KAPI_CTX_USER !=3D 0 { parts.push("KAPI_CTX_USER"); } + if flags & KAPI_CTX_KERNEL !=3D 0 { parts.push("KAPI_CTX_KERNEL");= } + if flags & KAPI_CTX_SLEEPABLE !=3D 0 { parts.push("KAPI_CTX_SLEEPA= BLE"); } + if flags & KAPI_CTX_ATOMIC !=3D 0 { parts.push("KAPI_CTX_ATOMIC");= } + if flags & KAPI_CTX_PREEMPTIBLE !=3D 0 { parts.push("KAPI_CTX_PREE= MPTIBLE"); } + if flags & KAPI_CTX_MIGRATION_DISABLED !=3D 0 { parts.push("KAPI_C= TX_MIGRATION_DISABLED"); } + + if !parts.is_empty() { + flag_strings.push(parts.join(" | ")); + } + flag_strings + } else { + vec![] + }; + + // Read parameter count + let param_count =3D reader.read_u32(); + + // Skip parameters for now (to match source output) + if let Some(count) =3D param_count { + if count > 0 && count <=3D sizes::MAX_PARAMS as u32 { + reader.skip(param_spec_layout_size() * count as usize); + reader.skip(param_spec_layout_size() * (sizes::MAX_PARAMS - co= unt as usize)); + } else { + reader.skip(param_spec_layout_size() * sizes::MAX_PARAMS); + } + } + + // Skip return spec + reader.skip(return_spec_layout_size()); + + // Read error count + let error_count =3D reader.read_u32(); + + // Skip errors + if let Some(count) =3D error_count { + if count > 0 && count <=3D sizes::MAX_ERRORS as u32 { + reader.skip(error_spec_layout_size() * count as usize); + reader.skip(error_spec_layout_size() * (sizes::MAX_ERRORS - co= unt as usize)); + } else { + reader.skip(error_spec_layout_size() * sizes::MAX_ERRORS); + } + } + + // Skip locks + if let Some(lock_count) =3D reader.read_u32() { + if lock_count > 0 && lock_count <=3D sizes::MAX_CONSTRAINTS as u32= { + reader.skip(lock_spec_layout_size() * lock_count as usize); + reader.skip(lock_spec_layout_size() * (sizes::MAX_CONSTRAINTS = - lock_count as usize)); + } else { + reader.skip(lock_spec_layout_size() * sizes::MAX_CONSTRAINTS); + } + } + + // Skip constraints + if let Some(constraint_count) =3D reader.read_u32() { + if constraint_count > 0 && constraint_count <=3D sizes::MAX_CONSTR= AINTS as u32 { + reader.skip(constraint_spec_layout_size() * constraint_count a= s usize); + reader.skip(constraint_spec_layout_size() * (sizes::MAX_CONSTR= AINTS - constraint_count as usize)); + } else { + reader.skip(constraint_spec_layout_size() * sizes::MAX_CONSTRA= INTS); + } + } + + // Read examples + let examples =3D reader.read_cstring(sizes::DESC * 2) + .filter(|s| !s.is_empty()); + + // Read notes + let notes =3D reader.read_cstring(sizes::DESC) + .filter(|s| !s.is_empty()); + + // Read since_version + let since_version =3D reader.read_cstring(32) + .filter(|s| !s.is_empty()); + + // Determine API type from name + let api_type =3D if name.starts_with("sys_") { + "syscall" + } else if name.contains("ioctl") || name.contains("IOCTL") { + "ioctl" + } else { + "other" + }.to_string(); + + Ok(ApiSpec { + name, + api_type, + description, + long_description, + version, + context_flags, + param_count, + error_count, + examples, + notes, + since_version, + }) +} + +// Old display_api_details_from_binary function removed - now using parse_= binary_to_api_spec + display_api_spec \ No newline at end of file diff --git a/tools/kapi/src/formatter/json.rs b/tools/kapi/src/formatter/js= on.rs new file mode 100644 index 0000000000000..44d2bbfc91133 --- /dev/null +++ b/tools/kapi/src/formatter/json.rs @@ -0,0 +1,170 @@ +use super::OutputFormatter; +use std::io::Write; +use serde::Serialize; + +pub struct JsonFormatter { + data: JsonData, +} + +#[derive(Serialize)] +struct JsonData { + #[serde(skip_serializing_if =3D "Option::is_none")] + apis: Option>, + #[serde(skip_serializing_if =3D "Option::is_none")] + api_details: Option, +} + +#[derive(Serialize)] +struct JsonApi { + name: String, + api_type: String, +} + +#[derive(Serialize)] +struct JsonApiDetails { + name: String, + #[serde(skip_serializing_if =3D "Option::is_none")] + description: Option, + #[serde(skip_serializing_if =3D "Option::is_none")] + long_description: Option, + #[serde(skip_serializing_if =3D "Vec::is_empty")] + context_flags: Vec, + #[serde(skip_serializing_if =3D "Option::is_none")] + examples: Option, + #[serde(skip_serializing_if =3D "Option::is_none")] + notes: Option, + #[serde(skip_serializing_if =3D "Option::is_none")] + since_version: Option, +} + + +impl JsonFormatter { + pub fn new() -> Self { + JsonFormatter { + data: JsonData { + apis: None, + api_details: None, + } + } + } +} + +impl OutputFormatter for JsonFormatter { + fn begin_document(&mut self, _w: &mut dyn Write) -> std::io::Result<()= > { + Ok(()) + } + + fn end_document(&mut self, w: &mut dyn Write) -> std::io::Result<()> { + let json =3D serde_json::to_string_pretty(&self.data)?; + writeln!(w, "{}", json)?; + Ok(()) + } + + fn begin_api_list(&mut self, _w: &mut dyn Write, _title: &str) -> std:= :io::Result<()> { + self.data.apis =3D Some(Vec::new()); + Ok(()) + } + + fn api_item(&mut self, _w: &mut dyn Write, name: &str, api_type: &str)= -> std::io::Result<()> { + if let Some(apis) =3D &mut self.data.apis { + apis.push(JsonApi { + name: name.to_string(), + api_type: api_type.to_string(), + }); + } + Ok(()) + } + + fn end_api_list(&mut self, _w: &mut dyn Write) -> std::io::Result<()> { + Ok(()) + } + + fn total_specs(&mut self, _w: &mut dyn Write, _count: usize) -> std::i= o::Result<()> { + Ok(()) + } + + fn begin_api_details(&mut self, _w: &mut dyn Write, name: &str) -> std= ::io::Result<()> { + self.data.api_details =3D Some(JsonApiDetails { + name: name.to_string(), + description: None, + long_description: None, + context_flags: Vec::new(), + examples: None, + notes: None, + since_version: None, + }); + Ok(()) + } + + fn end_api_details(&mut self, _w: &mut dyn Write) -> std::io::Result<(= )> { + Ok(()) + } + + + fn description(&mut self, _w: &mut dyn Write, desc: &str) -> std::io::= Result<()> { + if let Some(details) =3D &mut self.data.api_details { + details.description =3D Some(desc.to_string()); + } + Ok(()) + } + + fn long_description(&mut self, _w: &mut dyn Write, desc: &str) -> std:= :io::Result<()> { + if let Some(details) =3D &mut self.data.api_details { + details.long_description =3D Some(desc.to_string()); + } + Ok(()) + } + + fn begin_context_flags(&mut self, _w: &mut dyn Write) -> std::io::Resu= lt<()> { + Ok(()) + } + + fn context_flag(&mut self, _w: &mut dyn Write, flag: &str) -> std::io:= :Result<()> { + if let Some(details) =3D &mut self.data.api_details { + details.context_flags.push(flag.to_string()); + } + Ok(()) + } + + fn end_context_flags(&mut self, _w: &mut dyn Write) -> std::io::Result= <()> { + Ok(()) + } + + fn begin_parameters(&mut self, _w: &mut dyn Write, _count: u32) -> std= ::io::Result<()> { + Ok(()) + } + + + fn end_parameters(&mut self, _w: &mut dyn Write) -> std::io::Result<()= > { + Ok(()) + } + + fn begin_errors(&mut self, _w: &mut dyn Write, _count: u32) -> std::io= ::Result<()> { + Ok(()) + } + + fn end_errors(&mut self, _w: &mut dyn Write) -> std::io::Result<()> { + Ok(()) + } + + fn examples(&mut self, _w: &mut dyn Write, examples: &str) -> std::io:= :Result<()> { + if let Some(details) =3D &mut self.data.api_details { + details.examples =3D Some(examples.to_string()); + } + Ok(()) + } + + fn notes(&mut self, _w: &mut dyn Write, notes: &str) -> std::io::Resul= t<()> { + if let Some(details) =3D &mut self.data.api_details { + details.notes =3D Some(notes.to_string()); + } + Ok(()) + } + + fn since_version(&mut self, _w: &mut dyn Write, version: &str) -> std:= :io::Result<()> { + if let Some(details) =3D &mut self.data.api_details { + details.since_version =3D Some(version.to_string()); + } + Ok(()) + } +} \ No newline at end of file diff --git a/tools/kapi/src/formatter/mod.rs b/tools/kapi/src/formatter/mod= .rs new file mode 100644 index 0000000000000..6eb42e8b404d0 --- /dev/null +++ b/tools/kapi/src/formatter/mod.rs @@ -0,0 +1,68 @@ +use std::io::Write; + +mod plain; +mod json; +mod rst; + +pub use plain::PlainFormatter; +pub use json::JsonFormatter; +pub use rst::RstFormatter; + + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum OutputFormat { + Plain, + Json, + Rst, +} + +impl std::str::FromStr for OutputFormat { + type Err =3D String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "plain" =3D> Ok(OutputFormat::Plain), + "json" =3D> Ok(OutputFormat::Json), + "rst" =3D> Ok(OutputFormat::Rst), + _ =3D> Err(format!("Unknown output format: {}", s)), + } + } +} + +pub trait OutputFormatter { + fn begin_document(&mut self, w: &mut dyn Write) -> std::io::Result<()>; + fn end_document(&mut self, w: &mut dyn Write) -> std::io::Result<()>; + + fn begin_api_list(&mut self, w: &mut dyn Write, title: &str) -> std::i= o::Result<()>; + fn api_item(&mut self, w: &mut dyn Write, name: &str, api_type: &str) = -> std::io::Result<()>; + fn end_api_list(&mut self, w: &mut dyn Write) -> std::io::Result<()>; + + fn total_specs(&mut self, w: &mut dyn Write, count: usize) -> std::io:= :Result<()>; + + fn begin_api_details(&mut self, w: &mut dyn Write, name: &str) -> std:= :io::Result<()>; + fn end_api_details(&mut self, w: &mut dyn Write) -> std::io::Result<()= >; + fn description(&mut self, w: &mut dyn Write, desc: &str) -> std::io::R= esult<()>; + fn long_description(&mut self, w: &mut dyn Write, desc: &str) -> std::= io::Result<()>; + + fn begin_context_flags(&mut self, w: &mut dyn Write) -> std::io::Resul= t<()>; + fn context_flag(&mut self, w: &mut dyn Write, flag: &str) -> std::io::= Result<()>; + fn end_context_flags(&mut self, w: &mut dyn Write) -> std::io::Result<= ()>; + + fn begin_parameters(&mut self, w: &mut dyn Write, count: u32) -> std::= io::Result<()>; + fn end_parameters(&mut self, w: &mut dyn Write) -> std::io::Result<()>; + + fn begin_errors(&mut self, w: &mut dyn Write, count: u32) -> std::io::= Result<()>; + fn end_errors(&mut self, w: &mut dyn Write) -> std::io::Result<()>; + + fn examples(&mut self, w: &mut dyn Write, examples: &str) -> std::io::= Result<()>; + fn notes(&mut self, w: &mut dyn Write, notes: &str) -> std::io::Result= <()>; + fn since_version(&mut self, w: &mut dyn Write, version: &str) -> std::= io::Result<()>; +} + +pub fn create_formatter(format: OutputFormat) -> Box { + match format { + OutputFormat::Plain =3D> Box::new(PlainFormatter::new()), + OutputFormat::Json =3D> Box::new(JsonFormatter::new()), + OutputFormat::Rst =3D> Box::new(RstFormatter::new()), + } +} \ No newline at end of file diff --git a/tools/kapi/src/formatter/plain.rs b/tools/kapi/src/formatter/p= lain.rs new file mode 100644 index 0000000000000..4ccbfcbbc8416 --- /dev/null +++ b/tools/kapi/src/formatter/plain.rs @@ -0,0 +1,99 @@ +use super::OutputFormatter; +use std::io::Write; + +pub struct PlainFormatter; + +impl PlainFormatter { + pub fn new() -> Self { + PlainFormatter + } +} + +impl OutputFormatter for PlainFormatter { + fn begin_document(&mut self, _w: &mut dyn Write) -> std::io::Result<()= > { + Ok(()) + } + + fn end_document(&mut self, _w: &mut dyn Write) -> std::io::Result<()> { + Ok(()) + } + + fn begin_api_list(&mut self, w: &mut dyn Write, title: &str) -> std::i= o::Result<()> { + writeln!(w, "\n{}:", title)?; + writeln!(w, "{}", "-".repeat(title.len() + 1)) + } + + fn api_item(&mut self, w: &mut dyn Write, name: &str, _api_type: &str)= -> std::io::Result<()> { + writeln!(w, " {}", name) + } + + fn end_api_list(&mut self, _w: &mut dyn Write) -> std::io::Result<()> { + Ok(()) + } + + fn total_specs(&mut self, w: &mut dyn Write, count: usize) -> std::io:= :Result<()> { + writeln!(w, "\nTotal specifications found: {}", count) + } + + fn begin_api_details(&mut self, w: &mut dyn Write, name: &str) -> std:= :io::Result<()> { + writeln!(w, "\nDetailed information for {}:", name)?; + writeln!(w, "{}=3D", "=3D".repeat(25 + name.len())) + } + + fn end_api_details(&mut self, _w: &mut dyn Write) -> std::io::Result<(= )> { + Ok(()) + } + + + fn description(&mut self, w: &mut dyn Write, desc: &str) -> std::io::R= esult<()> { + writeln!(w, "Description: {}", desc) + } + + fn long_description(&mut self, w: &mut dyn Write, desc: &str) -> std::= io::Result<()> { + writeln!(w, "\nDetailed Description:")?; + writeln!(w, "{}", desc) + } + + fn begin_context_flags(&mut self, w: &mut dyn Write) -> std::io::Resul= t<()> { + writeln!(w, "\nExecution Context:") + } + + fn context_flag(&mut self, w: &mut dyn Write, flag: &str) -> std::io::= Result<()> { + writeln!(w, " - {}", flag) + } + + fn end_context_flags(&mut self, _w: &mut dyn Write) -> std::io::Result= <()> { + Ok(()) + } + + fn begin_parameters(&mut self, w: &mut dyn Write, count: u32) -> std::= io::Result<()> { + writeln!(w, "\nParameters ({}):", count) + } + + + fn end_parameters(&mut self, _w: &mut dyn Write) -> std::io::Result<()= > { + Ok(()) + } + + fn begin_errors(&mut self, w: &mut dyn Write, count: u32) -> std::io::= Result<()> { + writeln!(w, "\nPossible Errors ({}):", count) + } + + fn end_errors(&mut self, _w: &mut dyn Write) -> std::io::Result<()> { + Ok(()) + } + + fn examples(&mut self, w: &mut dyn Write, examples: &str) -> std::io::= Result<()> { + writeln!(w, "\nExamples:")?; + writeln!(w, "{}", examples) + } + + fn notes(&mut self, w: &mut dyn Write, notes: &str) -> std::io::Result= <()> { + writeln!(w, "\nNotes:")?; + writeln!(w, "{}", notes) + } + + fn since_version(&mut self, w: &mut dyn Write, version: &str) -> std::= io::Result<()> { + writeln!(w, "\nAvailable since: {}", version) + } +} \ No newline at end of file diff --git a/tools/kapi/src/formatter/rst.rs b/tools/kapi/src/formatter/rst= .rs new file mode 100644 index 0000000000000..96be83bf208dd --- /dev/null +++ b/tools/kapi/src/formatter/rst.rs @@ -0,0 +1,144 @@ +use super::OutputFormatter; +use std::io::Write; + +pub struct RstFormatter { + current_section_level: usize, +} + +impl RstFormatter { + pub fn new() -> Self { + RstFormatter { + current_section_level: 0, + } + } + + fn section_char(&self, level: usize) -> char { + match level { + 0 =3D> '=3D', + 1 =3D> '-', + 2 =3D> '~', + 3 =3D> '^', + _ =3D> '"', + } + } +} + +impl OutputFormatter for RstFormatter { + fn begin_document(&mut self, _w: &mut dyn Write) -> std::io::Result<()= > { + Ok(()) + } + + fn end_document(&mut self, _w: &mut dyn Write) -> std::io::Result<()> { + Ok(()) + } + + fn begin_api_list(&mut self, w: &mut dyn Write, title: &str) -> std::i= o::Result<()> { + writeln!(w, "\n{}", title)?; + writeln!(w, "{}", self.section_char(0).to_string().repeat(title.le= n()))?; + writeln!(w) + } + + fn api_item(&mut self, w: &mut dyn Write, name: &str, api_type: &str) = -> std::io::Result<()> { + writeln!(w, "* **{}** (*{}*)", name, api_type) + } + + fn end_api_list(&mut self, _w: &mut dyn Write) -> std::io::Result<()> { + Ok(()) + } + + fn total_specs(&mut self, w: &mut dyn Write, count: usize) -> std::io:= :Result<()> { + writeln!(w, "\n**Total specifications found:** {}", count) + } + + fn begin_api_details(&mut self, w: &mut dyn Write, name: &str) -> std:= :io::Result<()> { + self.current_section_level =3D 0; + writeln!(w, "\n{}", name)?; + writeln!(w, "{}", self.section_char(0).to_string().repeat(name.len= ()))?; + writeln!(w) + } + + fn end_api_details(&mut self, _w: &mut dyn Write) -> std::io::Result<(= )> { + Ok(()) + } + + + fn description(&mut self, w: &mut dyn Write, desc: &str) -> std::io::R= esult<()> { + writeln!(w, "**{}**", desc)?; + writeln!(w) + } + + fn long_description(&mut self, w: &mut dyn Write, desc: &str) -> std::= io::Result<()> { + writeln!(w, "{}", desc)?; + writeln!(w) + } + + fn begin_context_flags(&mut self, w: &mut dyn Write) -> std::io::Resul= t<()> { + self.current_section_level =3D 1; + let title =3D "Execution Context"; + writeln!(w, "{}", title)?; + writeln!(w, "{}", self.section_char(1).to_string().repeat(title.le= n()))?; + writeln!(w) + } + + fn context_flag(&mut self, w: &mut dyn Write, flag: &str) -> std::io::= Result<()> { + writeln!(w, "* {}", flag) + } + + fn end_context_flags(&mut self, w: &mut dyn Write) -> std::io::Result<= ()> { + writeln!(w) + } + + fn begin_parameters(&mut self, w: &mut dyn Write, count: u32) -> std::= io::Result<()> { + self.current_section_level =3D 1; + let title =3D format!("Parameters ({})", count); + writeln!(w, "{}", title)?; + writeln!(w, "{}", self.section_char(1).to_string().repeat(title.le= n()))?; + writeln!(w) + } + + + fn end_parameters(&mut self, _w: &mut dyn Write) -> std::io::Result<()= > { + Ok(()) + } + + fn begin_errors(&mut self, w: &mut dyn Write, count: u32) -> std::io::= Result<()> { + self.current_section_level =3D 1; + let title =3D format!("Possible Errors ({})", count); + writeln!(w, "{}", title)?; + writeln!(w, "{}", self.section_char(1).to_string().repeat(title.le= n()))?; + writeln!(w) + } + + fn end_errors(&mut self, _w: &mut dyn Write) -> std::io::Result<()> { + Ok(()) + } + + fn examples(&mut self, w: &mut dyn Write, examples: &str) -> std::io::= Result<()> { + self.current_section_level =3D 1; + let title =3D "Examples"; + writeln!(w, "{}", title)?; + writeln!(w, "{}", self.section_char(1).to_string().repeat(title.le= n()))?; + writeln!(w)?; + writeln!(w, ".. code-block:: c")?; + writeln!(w)?; + for line in examples.lines() { + writeln!(w, " {}", line)?; + } + writeln!(w) + } + + fn notes(&mut self, w: &mut dyn Write, notes: &str) -> std::io::Result= <()> { + self.current_section_level =3D 1; + let title =3D "Notes"; + writeln!(w, "{}", title)?; + writeln!(w, "{}", self.section_char(1).to_string().repeat(title.le= n()))?; + writeln!(w)?; + writeln!(w, "{}", notes)?; + writeln!(w) + } + + fn since_version(&mut self, w: &mut dyn Write, version: &str) -> std::= io::Result<()> { + writeln!(w, ":Available since: {}", version)?; + writeln!(w) + } +} \ No newline at end of file diff --git a/tools/kapi/src/main.rs b/tools/kapi/src/main.rs new file mode 100644 index 0000000000000..9d6533cbc7dd1 --- /dev/null +++ b/tools/kapi/src/main.rs @@ -0,0 +1,121 @@ +//! kapi - Kernel API Specification Tool +//! +//! This tool extracts and displays kernel API specifications from multipl= e sources: +//! - Kernel source code (KAPI macros) +//! - Compiled vmlinux binaries (.kapi_specs ELF section) +//! - Running kernel via debugfs + +use anyhow::Result; +use clap::Parser; +use std::io::{self, Write}; + +mod formatter; +mod extractor; + +use formatter::{OutputFormat, create_formatter}; +use extractor::{ApiExtractor, VmlinuxExtractor, SourceExtractor, DebugfsEx= tractor}; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about =3D None)] +struct Args { + /// Path to the vmlinux file + #[arg(long, value_name =3D "PATH", group =3D "input")] + vmlinux: Option, + + /// Path to kernel source directory or file + #[arg(long, value_name =3D "PATH", group =3D "input")] + source: Option, + + /// Path to debugfs (defaults to /sys/kernel/debug if not specified) + #[arg(long, value_name =3D "PATH", group =3D "input")] + debugfs: Option, + + /// Optional: Name of specific API to show details for + api_name: Option, + + /// Output format + #[arg(long, short =3D 'f', default_value =3D "plain")] + format: String, +} + +fn main() -> Result<()> { + let args =3D Args::parse(); + + let output_format: OutputFormat =3D args.format.parse() + .map_err(|e: String| anyhow::anyhow!(e))?; + + let extractor: Box =3D match (args.vmlinux, args.sou= rce, args.debugfs.clone()) { + (Some(vmlinux_path), None, None) =3D> { + Box::new(VmlinuxExtractor::new(vmlinux_path)?) + } + (None, Some(source_path), None) =3D> { + Box::new(SourceExtractor::new(source_path)?) + } + (None, None, Some(_)) | (None, None, None) =3D> { + // If debugfs is specified or no input is provided, use debugfs + Box::new(DebugfsExtractor::new(args.debugfs)?) + } + _ =3D> { + anyhow::bail!("Please specify only one of --vmlinux, --source,= or --debugfs") + } + }; + + display_apis(extractor.as_ref(), args.api_name, output_format) +} + +fn display_apis(extractor: &dyn ApiExtractor, api_name: Option, ou= tput_format: OutputFormat) -> Result<()> { + let mut formatter =3D create_formatter(output_format); + let mut stdout =3D io::stdout(); + + formatter.begin_document(&mut stdout)?; + + if let Some(api_name_req) =3D api_name { + // Use the extractor to display API details + if let Some(_spec) =3D extractor.extract_by_name(&api_name_req)? { + extractor.display_api_details(&api_name_req, &mut *formatter, = &mut stdout)?; + } else if output_format =3D=3D OutputFormat::Plain { + writeln!(stdout, "\nAPI '{}' not found.", api_name_req)?; + writeln!(stdout, "\nAvailable APIs:")?; + for spec in extractor.extract_all()? { + writeln!(stdout, " {} ({})", spec.name, spec.api_type)?; + } + } + } else { + // Display list of APIs using the extractor + let all_specs =3D extractor.extract_all()?; + let syscalls: Vec<_> =3D all_specs.iter().filter(|s| s.api_type = =3D=3D "syscall").collect(); + let ioctls: Vec<_> =3D all_specs.iter().filter(|s| s.api_type =3D= =3D "ioctl").collect(); + let functions: Vec<_> =3D all_specs.iter().filter(|s| s.api_type = =3D=3D "function").collect(); + + if !syscalls.is_empty() { + formatter.begin_api_list(&mut stdout, "System Calls")?; + for spec in syscalls { + formatter.api_item(&mut stdout, &spec.name, &spec.api_type= )?; + } + formatter.end_api_list(&mut stdout)?; + } + + if !ioctls.is_empty() { + formatter.begin_api_list(&mut stdout, "IOCTLs")?; + for spec in ioctls { + formatter.api_item(&mut stdout, &spec.name, &spec.api_type= )?; + } + formatter.end_api_list(&mut stdout)?; + } + + if !functions.is_empty() { + formatter.begin_api_list(&mut stdout, "Functions")?; + for spec in functions { + formatter.api_item(&mut stdout, &spec.name, &spec.api_type= )?; + } + formatter.end_api_list(&mut stdout)?; + } + + formatter.total_specs(&mut stdout, all_specs.len())?; + } + + formatter.end_document(&mut stdout)?; + + Ok(()) +} + --=20 2.39.5