diff options
| author | Mark Brown <broonie@kernel.org> | 2026-10-10 01:31:20 +0100 |
|---|---|---|
| committer | Mark Brown <broonie@kernel.org> | 2026-10-10 01:31:20 +0100 |
| commit | dba9716e82ae2c9eacfdc0925360375cd0bb6830 (patch) | |
| tree | b0a41f80f6a35a4be3c607e3957a9d08718baca8 /tools/kapi/src/extractor/debugfs.rs | |
| parent | c148298f22c67c053bf6507ed092e0ae58a4b2a8 (diff) | |
| parent | ef777ef05ebcc5ea5393b4bd5af6fc3ad1b3aec9 (diff) | |
| download | linux-next-dba9716e82ae2c9eacfdc0925360375cd0bb6830.tar.gz linux-next-dba9716e82ae2c9eacfdc0925360375cd0bb6830.zip | |
Merge branch 'spec' of https://git.kernel.org/pub/scm/linux/kernel/git/sashal/kapi.git
# Conflicts:
# Makefile
# fs/open.c
Diffstat (limited to 'tools/kapi/src/extractor/debugfs.rs')
| -rw-r--r-- | tools/kapi/src/extractor/debugfs.rs | 1704 |
1 files changed, 1704 insertions, 0 deletions
diff --git a/tools/kapi/src/extractor/debugfs.rs b/tools/kapi/src/extractor/debugfs.rs new file mode 100644 index 000000000000..0a7e41e1c40c --- /dev/null +++ b/tools/kapi/src/extractor/debugfs.rs @@ -0,0 +1,1704 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (C) 2026 Sasha Levin <sashal@kernel.org> + +use crate::formatter::OutputFormatter; +use anyhow::{bail, Context, Result}; +use serde::Deserialize; +use std::fs; +use std::io::Write; +use std::path::PathBuf; + +use super::{ + display_api_spec, ApiExtractor, ApiSpec, CapabilitySpec, ConstraintSpec, ErrorSpec, LockSpec, + ParamSpec, ReturnSpec, SignalMaskSpec, StateTransitionSpec, StructFieldSpec, StructSpec, +}; + +// Schema matching what kapi_export_json() in the kernel emits. The kernel +// serialises several enum-like fields as hex strings ("0x%x") or token +// strings ("exact", "process"); we keep them as Option<String> here and +// interpret them during conversion. +#[derive(Deserialize)] +struct KernelApiJson { + name: String, + #[serde(default)] + api_type: Option<String>, + #[serde(default)] + version: Option<u32>, + #[serde(default)] + description: Option<String>, + #[serde(default)] + long_description: Option<String>, + #[serde(default)] + context_flags: Option<String>, + #[serde(default)] + examples: Option<String>, + #[serde(default)] + notes: Option<String>, + #[serde(default)] + capabilities: Option<Vec<KernelCapabilityJson>>, + #[serde(default)] + parameters: Option<Vec<KernelParamJson>>, + #[serde(default)] + errors: Option<Vec<KernelErrorJson>>, + #[serde(default, rename = "return")] + return_spec: Option<KernelReturnJson>, + #[serde(default)] + locks: Option<Vec<KernelLockJson>>, + #[serde(default)] + constraints: Option<Vec<KernelConstraintJson>>, + #[serde(default)] + signals: Option<Vec<KernelSignalJson>>, + #[serde(default)] + side_effects: Option<Vec<KernelSideEffectJson>>, + #[serde(default)] + state_transitions: Option<Vec<KernelStateTransitionJson>>, + #[serde(default)] + signal_masks: Option<Vec<KernelSignalMaskJson>>, + #[serde(default)] + struct_specs: Option<Vec<KernelStructJson>>, +} + +#[derive(Deserialize)] +struct KernelStateTransitionJson { + #[serde(default)] + object: Option<String>, + #[serde(default)] + from_state: Option<String>, + #[serde(default)] + to_state: Option<String>, + #[serde(default)] + condition: Option<String>, + #[serde(default)] + description: Option<String>, +} + +#[derive(Deserialize)] +struct KernelSignalMaskJson { + #[serde(default)] + name: Option<String>, + #[serde(default)] + description: Option<String>, + #[serde(default)] + signals: Vec<i32>, +} + +#[derive(Deserialize)] +struct KernelStructFieldJson { + #[serde(default)] + name: Option<String>, + #[serde(rename = "type", default)] + type_name: Option<String>, + #[serde(default)] + type_class: Option<String>, + #[serde(default)] + offset: usize, + #[serde(default)] + size: usize, + #[serde(default)] + flags: Option<String>, + #[serde(default)] + constraint_type: Option<String>, + #[serde(default)] + min_value: i64, + #[serde(default)] + max_value: i64, + #[serde(default)] + valid_mask: Option<String>, + #[serde(default)] + description: Option<String>, +} + +#[derive(Deserialize)] +struct KernelStructJson { + #[serde(default)] + name: Option<String>, + #[serde(default)] + size: usize, + #[serde(default)] + alignment: usize, + #[serde(default)] + description: Option<String>, + #[serde(default)] + fields: Vec<KernelStructFieldJson>, +} + +#[derive(Deserialize)] +struct KernelConstraintJson { + name: String, + #[serde(default)] + description: Option<String>, + #[serde(default)] + expression: Option<String>, +} + +#[derive(Deserialize)] +struct KernelSignalJson { + #[serde(default)] + signal_num: i32, + #[serde(default)] + signal_name: Option<String>, + #[serde(default)] + direction: Option<String>, + #[serde(default)] + action: u32, + #[serde(default)] + target: Option<String>, + #[serde(default)] + condition: Option<String>, + #[serde(default)] + description: Option<String>, + #[serde(default)] + restartable: bool, + #[serde(default)] + sa_flags_required: Option<String>, + #[serde(default)] + sa_flags_forbidden: Option<String>, + #[serde(default)] + error_on_signal: i32, + #[serde(default)] + transform_to: i32, + #[serde(default)] + timing: Option<String>, + #[serde(default)] + priority: u32, + #[serde(default)] + interruptible: bool, + #[serde(default)] + queue_behavior: Option<String>, + #[serde(default)] + state_required: Option<String>, + #[serde(default)] + state_forbidden: Option<String>, +} + +#[derive(Deserialize)] +struct KernelSideEffectJson { + #[serde(rename = "type", default)] + type_hex: Option<String>, + #[serde(default)] + target: Option<String>, + #[serde(default)] + condition: Option<String>, + #[serde(default)] + description: Option<String>, + #[serde(default)] + reversible: bool, +} + +#[derive(Deserialize)] +struct KernelParamJson { + name: String, + #[serde(rename = "type", default)] + type_name: Option<String>, + #[serde(default)] + type_class: Option<String>, + #[serde(default)] + description: Option<String>, + #[serde(default)] + flags: Option<String>, + #[serde(default)] + constraint_type: Option<String>, + #[serde(default)] + constraint_desc: Option<String>, + #[serde(default)] + min_value: Option<i64>, + #[serde(default)] + max_value: Option<i64>, + #[serde(default)] + valid_mask: Option<String>, + #[serde(default)] + enum_values: Vec<i64>, + #[serde(default)] + size: Option<u64>, + #[serde(default)] + alignment: Option<u64>, + #[serde(default)] + size_param_idx: Option<u32>, +} + +#[derive(Deserialize)] +struct KernelErrorJson { + #[serde(rename = "code")] + error_code: i32, + #[serde(default)] + name: Option<String>, + #[serde(default)] + condition: Option<String>, + #[serde(default)] + description: Option<String>, +} + +#[derive(Deserialize)] +struct KernelReturnJson { + #[serde(rename = "type", default)] + type_name: Option<String>, + #[serde(default)] + type_class: Option<String>, + #[serde(default)] + check_type: Option<String>, + #[serde(default)] + description: Option<String>, + #[serde(default)] + success_value: Option<i64>, + #[serde(default)] + success_min: Option<i64>, + #[serde(default)] + success_max: Option<i64>, + #[serde(default)] + error_values: Vec<i64>, +} + +#[derive(Deserialize)] +struct KernelLockJson { + name: String, + #[serde(rename = "type", default)] + lock_type: Option<String>, + #[serde(default)] + scope: Option<String>, + #[serde(default)] + description: Option<String>, +} + +#[derive(Deserialize)] +struct KernelCapabilityJson { + capability: i32, + name: String, + action: String, + allows: String, + without_cap: String, + check_condition: Option<String>, + priority: Option<u8>, + alternatives: Option<Vec<i32>>, +} + +/// Free-text fields of the text dump that may span several lines. +#[derive(Clone, Copy)] +enum TextField { + Description, + LongDescription, + Examples, + Notes, +} + +impl TextField { + /// Recognise the header line of a text field, returning the field and + /// whatever follows the colon on the same line. + fn start(line: &str) -> Option<(Self, &str)> { + [ + ("Description:", Self::Description), + ("Long description:", Self::LongDescription), + ("Examples:", Self::Examples), + ("Notes:", Self::Notes), + ] + .into_iter() + .find_map(|(header, field)| { + line.strip_prefix(header) + .map(|rest| (field, rest.trim_start())) + }) + } + + fn store(self, spec: &mut ApiSpec, lines: &[String]) { + let text = lines.join("\n"); + let text = text.trim_matches('\n').trim_end().to_string(); + let slot = match self { + Self::Description => &mut spec.description, + Self::LongDescription => &mut spec.long_description, + Self::Examples => &mut spec.examples, + Self::Notes => &mut spec.notes, + }; + *slot = Some(text); + } +} + +/// 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<String>) -> Result<Self> { + let path = match debugfs_path { + Some(p) => PathBuf::from(p), + None => 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 = path.join("kapi"); + if !kapi_path.exists() { + bail!( + "Kernel API debugfs interface not found at: {}", + kapi_path.display() + ); + } + + Ok(Self { debugfs_path: path }) + } + + /// Parse the list file to get all available API names + fn parse_list_file(&self) -> Result<Vec<String>> { + let list_path = self.debugfs_path.join("kapi/list"); + let content = fs::read_to_string(&list_path) + .with_context(|| format!("Failed to read {}", list_path.display()))?; + + let mut apis = Vec::new(); + let mut in_list = false; + + for line in content.lines() { + if line.contains("===") { + in_list = 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) = line.split(" - ").next() { + apis.push(name.trim().to_string()); + } + } + } + + Ok(apis) + } + + /// Convert context flags (emitted by the kernel as a hex string like + /// "0x21") into the token list consumed by the formatter. + fn parse_context_flags(flags: &str) -> Vec<String> { + let mut result = Vec::new(); + let bits = flags + .strip_prefix("0x") + .or_else(|| flags.strip_prefix("0X")) + .unwrap_or(flags); + let Ok(flags) = u32::from_str_radix(bits, 16) else { + return result; + }; + + // These values should match KAPI_CTX_* flags from kernel + if flags & (1 << 0) != 0 { + result.push("KAPI_CTX_PROCESS".to_string()); + } + if flags & (1 << 1) != 0 { + result.push("KAPI_CTX_SOFTIRQ".to_string()); + } + if flags & (1 << 2) != 0 { + result.push("KAPI_CTX_HARDIRQ".to_string()); + } + if flags & (1 << 3) != 0 { + result.push("KAPI_CTX_NMI".to_string()); + } + if flags & (1 << 4) != 0 { + result.push("KAPI_CTX_ATOMIC".to_string()); + } + if flags & (1 << 5) != 0 { + result.push("KAPI_CTX_SLEEPABLE".to_string()); + } + if flags & (1 << 6) != 0 { + result.push("KAPI_CTX_PREEMPT_DISABLED".to_string()); + } + if flags & (1 << 7) != 0 { + result.push("KAPI_CTX_IRQ_DISABLED".to_string()); + } + + result + } + + /// Parse a hex-string like "0x123" into u64, returning 0 on failure. + fn parse_hex_u64(value: &str) -> u64 { + let bits = value + .strip_prefix("0x") + .or_else(|| value.strip_prefix("0X")) + .unwrap_or(value); + u64::from_str_radix(bits, 16).unwrap_or(0) + } + + /// Parse a hex-string like "0x123" into u32, returning 0 on failure. + fn parse_hex_u32(value: &str) -> u32 { + u32::try_from(Self::parse_hex_u64(value)).unwrap_or(0) + } + + /// Map the type-class token emitted by the kernel (param_type_to_string) + /// back to the numeric enum kapi_param_type. + fn parse_type_class(token: &str) -> u32 { + match token { + "void" => 0, + "int" => 1, + "uint" => 2, + "pointer" => 3, + "struct" => 4, + "union" => 5, + "enum" => 6, + "function_pointer" => 7, + "array" => 8, + "file_descriptor" => 9, + "user_pointer" => 10, + "pathname" => 11, + "custom" => 12, + _ => 0, + } + } + + /// Map the constraint-type token emitted by the kernel + /// (constraint_type_to_string) back to the numeric enum + /// kapi_constraint_type. + fn parse_constraint_type(token: &str) -> u32 { + match token { + "none" => 0, + "range" => 1, + "mask" => 2, + "enum" => 3, + "alignment" => 4, + "power_of_two" => 5, + "page_aligned" => 6, + "nonzero" => 7, + "user_string" => 8, + "user_path" => 9, + "user_ptr" => 10, + "buffer" => 11, + "custom" => 12, + _ => 0, + } + } + + /// Derive the API type from the symbol name, the same way the vmlinux + /// extractor does. + fn api_type_from_name(name: &str) -> &'static str { + if name.starts_with("sys_") { + "syscall" + } else if name.ends_with("_ioctl") { + "ioctl" + } else if name.contains("sysfs") { + "sysfs" + } else { + "function" + } + } + + /// Map the check-type token emitted by the kernel + /// (return_check_type_to_string) back to the u32 enum the formatter wants. + fn parse_check_type(token: &str) -> u32 { + match token { + "exact" => 0, + "range" => 1, + "error_check" => 2, + "file_descriptor" => 3, + "custom" => 4, + "no_return" => 5, + _ => 0, + } + } + + /// Map the lock-type token emitted by the kernel (lock_type_to_string). + fn parse_lock_type(token: &str) -> u32 { + match token { + "none" => 0, + "mutex" => 1, + "spinlock" => 2, + "rwlock" => 3, + "seqlock" => 4, + "rcu" => 5, + "semaphore" => 6, + "custom" => 7, + _ => 0, + } + } + + /// Map the lock-scope token emitted by the kernel (lock_scope_to_string). + fn parse_lock_scope(token: &str) -> u32 { + match token { + "internal" => 0, + "acquires" => 1, + "releases" => 2, + "caller_held" => 3, + _ => 0, + } + } + + /// Map the signal-timing token (e.g. "during") back to the u32 enum that + /// the source-side parser produces. Mirrors + /// KerneldocParser::parse_signal_timing in kerneldoc_parser.rs so the + /// debugfs path and the source path agree. + fn parse_signal_timing(token: &str) -> u32 { + match token.trim().to_ascii_lowercase().as_str() { + "before" => 0, + "during" => 1, + "after" => 2, + _ => 0, + } + } + + /// Convert the capability action token emitted by the kernel + /// (capability_action_to_string) to the KAPI_CAP_* spelling used by the + /// other extractors. + fn parse_capability_action(action: &str) -> String { + match action { + "bypass_check" => "KAPI_CAP_BYPASS_CHECK".to_string(), + "increase_limit" => "KAPI_CAP_INCREASE_LIMIT".to_string(), + "override_restriction" => "KAPI_CAP_OVERRIDE_RESTRICTION".to_string(), + "grant_permission" => "KAPI_CAP_GRANT_PERMISSION".to_string(), + "modify_behavior" => "KAPI_CAP_MODIFY_BEHAVIOR".to_string(), + "access_resource" => "KAPI_CAP_ACCESS_RESOURCE".to_string(), + "perform_operation" => "KAPI_CAP_PERFORM_OPERATION".to_string(), + _ => action.to_string(), + } + } + + /// Try to parse as JSON first + fn try_parse_json(&self, content: &str) -> Result<ApiSpec, serde_json::Error> { + let json_data: KernelApiJson = serde_json::from_str(content)?; + // The kernel-side kapi_json_str() emits NULL char * as the empty + // string "", so normalise empty -> None to match the ApiSpec + // convention used by --source / --vmlinux. + fn opt_str(s: Option<String>) -> Option<String> { + s.filter(|v| !v.is_empty()) + } + let api_type = json_data + .api_type + .unwrap_or_else(|| Self::api_type_from_name(&json_data.name).to_string()); + + let mut spec = ApiSpec { + name: json_data.name, + api_type, + description: opt_str(json_data.description), + long_description: opt_str(json_data.long_description), + version: json_data.version.map(|v| v.to_string()), + context_flags: json_data + .context_flags + .as_deref() + .map_or_else(Vec::new, Self::parse_context_flags), + param_count: None, + error_count: None, + examples: opt_str(json_data.examples), + notes: opt_str(json_data.notes), + subsystem: None, // Not in the JSON format + sysfs_path: None, // Not in the JSON format + permissions: None, // Not in the JSON format + capabilities: vec![], + parameters: vec![], + return_spec: None, + errors: vec![], + signals: vec![], + signal_masks: vec![], + side_effects: vec![], + state_transitions: vec![], + constraints: vec![], + locks: vec![], + struct_specs: vec![], + }; + + // Convert capabilities + if let Some(caps) = json_data.capabilities { + for cap in caps { + spec.capabilities.push(CapabilitySpec { + capability: cap.capability, + name: cap.name, + action: Self::parse_capability_action(&cap.action), + allows: cap.allows, + without_cap: cap.without_cap, + check_condition: opt_str(cap.check_condition), + priority: cap.priority, + alternatives: cap.alternatives.unwrap_or_default(), + }); + } + } + + // Convert parameters. + if let Some(params) = json_data.parameters { + for (i, p) in params.into_iter().enumerate() { + let flags = p.flags.as_deref().map_or(0, Self::parse_hex_u32); + let mut param = ParamSpec { + index: i as u32, + name: p.name, + type_name: p.type_name.unwrap_or_default(), + description: p.description.unwrap_or_default(), + flags, + param_type: p.type_class.as_deref().map_or(0, Self::parse_type_class), + constraint_type: p + .constraint_type + .as_deref() + .map_or(0, Self::parse_constraint_type), + constraint: opt_str(p.constraint_desc), + min_value: p.min_value, + max_value: p.max_value, + valid_mask: p.valid_mask.as_deref().map(Self::parse_hex_u64), + enum_values: p.enum_values.iter().map(i64::to_string).collect(), + size: p.size.map(|v| v as u32), + alignment: p.alignment.map(|v| v as u32), + size_param_idx: p.size_param_idx, + }; + param.keep_used_numbers(); + spec.parameters.push(param); + } + if !spec.parameters.is_empty() { + spec.param_count = Some(spec.parameters.len() as u32); + } + } + + // Convert errors + if let Some(errors) = json_data.errors { + for e in errors { + spec.errors.push(ErrorSpec { + error_code: e.error_code, + name: e.name.unwrap_or_default(), + condition: e.condition.unwrap_or_default(), + description: e.description.unwrap_or_default(), + }); + } + if !spec.errors.is_empty() { + spec.error_count = Some(spec.errors.len() as u32); + } + } + + // Convert return spec + if let Some(ret) = json_data.return_spec { + let check_type = ret.check_type.as_deref().map_or(0, Self::parse_check_type); + let return_type = ret.type_class.as_deref().map_or(0, Self::parse_type_class); + let type_name = ret.type_name.unwrap_or_default(); + let success_value = ret.success_value.unwrap_or(0); + // A spec without a return block exports as an all-zero return + // spec; report it as absent like the vmlinux extractor does. + if !(type_name.is_empty() && return_type == 0 && check_type == 0 && success_value == 0) + { + let mut ret_spec = ReturnSpec { + type_name, + description: ret.description.unwrap_or_default(), + return_type, + check_type, + success_value: ret.success_value, + success_min: ret.success_min, + success_max: ret.success_max, + error_values: ret + .error_values + .into_iter() + .filter_map(|v| i32::try_from(v).ok()) + .collect(), + }; + ret_spec.keep_used_success_fields(); + spec.return_spec = Some(ret_spec); + } + } + + // Convert locks + if let Some(locks) = json_data.locks { + for l in locks { + let lock_type = l.lock_type.as_deref().map_or(0, Self::parse_lock_type); + let scope = l.scope.as_deref().map_or(0, Self::parse_lock_scope); + spec.locks.push(LockSpec { + lock_name: l.name, + lock_type, + scope, + description: l.description.unwrap_or_default(), + }); + } + } + + // Convert constraints. Empty strings emitted from kapi_json_str() + // for NULL char * fields normalise back to None to match --source. + if let Some(constraints) = json_data.constraints { + for c in constraints { + spec.constraints.push(ConstraintSpec { + name: c.name, + description: c.description.unwrap_or_default(), + expression: c.expression.filter(|v| !v.is_empty()), + }); + } + } + + // Convert signals. + if let Some(signals) = json_data.signals { + for s in signals { + let direction = s.direction.as_deref().map_or(0, Self::parse_hex_u32); + let sa_flags_required = s + .sa_flags_required + .as_deref() + .map_or(0, Self::parse_hex_u32); + let sa_flags_forbidden = s + .sa_flags_forbidden + .as_deref() + .map_or(0, Self::parse_hex_u32); + let state_required = s.state_required.as_deref().map_or(0, Self::parse_hex_u32); + let state_forbidden = s.state_forbidden.as_deref().map_or(0, Self::parse_hex_u32); + let timing = s.timing.as_deref().map_or(0, Self::parse_signal_timing); + spec.signals.push(super::SignalSpec { + signal_num: s.signal_num, + signal_name: s.signal_name.unwrap_or_default(), + direction, + action: s.action, + target: opt_str(s.target), + condition: opt_str(s.condition), + description: opt_str(s.description), + timing, + priority: s.priority, + restartable: s.restartable, + interruptible: s.interruptible, + queue: opt_str(s.queue_behavior), + sa_flags: 0, + sa_flags_required, + sa_flags_forbidden, + state_required, + state_forbidden, + error_on_signal: if s.error_on_signal != 0 { + Some(s.error_on_signal) + } else { + None + }, + transform_to: if s.transform_to != 0 { + // Kernel JSON already carries the numeric value. + Some(s.transform_to) + } else { + None + }, + }); + } + } + + // Convert side effects. + if let Some(effects) = json_data.side_effects { + for e in effects { + let effect_type = e.type_hex.as_deref().map_or(0, Self::parse_hex_u32); + spec.side_effects.push(super::SideEffectSpec { + effect_type, + target: e.target.unwrap_or_default(), + condition: e.condition.filter(|v| !v.is_empty()), + description: e.description.unwrap_or_default(), + reversible: e.reversible, + }); + } + } + + if let Some(transitions) = json_data.state_transitions { + for t in transitions { + spec.state_transitions.push(StateTransitionSpec { + object: t.object.unwrap_or_default(), + from_state: t.from_state.unwrap_or_default(), + to_state: t.to_state.unwrap_or_default(), + condition: opt_str(t.condition), + description: t.description.unwrap_or_default(), + }); + } + } + + if let Some(masks) = json_data.signal_masks { + for m in masks { + spec.signal_masks.push(SignalMaskSpec { + name: m.name.unwrap_or_default(), + description: m.description.unwrap_or_default(), + signals: m.signals, + }); + } + } + + if let Some(structs) = json_data.struct_specs { + for st in structs { + let fields: Vec<StructFieldSpec> = st + .fields + .into_iter() + .map(|f| StructFieldSpec { + name: f.name.unwrap_or_default(), + field_type: f.type_class.as_deref().map_or(0, Self::parse_type_class), + type_name: f.type_name.unwrap_or_default(), + offset: f.offset, + size: f.size, + flags: f.flags.as_deref().map_or(0, Self::parse_hex_u32), + constraint_type: f + .constraint_type + .as_deref() + .map_or(0, Self::parse_constraint_type), + min_value: f.min_value, + max_value: f.max_value, + valid_mask: f.valid_mask.as_deref().map_or(0, Self::parse_hex_u64), + description: f.description.unwrap_or_default(), + }) + .collect(); + spec.struct_specs.push(StructSpec { + name: st.name.unwrap_or_default(), + size: st.size, + alignment: st.alignment, + field_count: fields.len() as u32, + fields, + description: st.description.unwrap_or_default(), + }); + } + } + + Ok(spec) + } + + /// Parse a single API specification file + fn parse_spec_file(&self, api_name: &str) -> Result<ApiSpec> { + // Prefer the JSON endpoint; fall back to the plain-text dump under + // kapi/specs/ if it is missing or does not parse. + let json_path = self + .debugfs_path + .join(format!("kapi/specs-json/{}", api_name)); + match fs::read_to_string(&json_path) { + Ok(content) => match self.try_parse_json(&content) { + Ok(spec) => return Ok(spec), + Err(e) => eprintln!( + "Warning: invalid JSON in {}: {}; using the text dump", + json_path.display(), + e + ), + }, + Err(e) if e.kind() != std::io::ErrorKind::NotFound => eprintln!( + "Warning: cannot read {}: {}; using the text dump", + json_path.display(), + e + ), + Err(_) => {} + } + + let spec_path = self.debugfs_path.join(format!("kapi/specs/{}", api_name)); + let content = fs::read_to_string(&spec_path) + .with_context(|| format!("Failed to read {}", spec_path.display()))?; + + // The specs/ file may hold JSON as well. + if let Ok(spec) = self.try_parse_json(&content) { + return Ok(spec); + } + + // Fall back to plain text parsing + let mut spec = 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, + subsystem: None, + sysfs_path: None, + permissions: None, + capabilities: vec![], + parameters: vec![], + return_spec: None, + errors: vec![], + signals: vec![], + signal_masks: vec![], + side_effects: vec![], + state_transitions: vec![], + constraints: vec![], + locks: vec![], + struct_specs: vec![], + }; + + // Parse the content + let mut text_field: Option<TextField> = None; + let mut text_lines: Vec<String> = Vec::new(); + let mut parsing_capability = false; + let mut in_capabilities_section = false; + let mut current_capability: Option<CapabilitySpec> = None; + + for line in content.lines() { + // The kernel indents every line of a multi-line value, so a value + // ends at the first non-blank line that is not indented. + if let Some(field) = text_field { + if line.is_empty() { + text_lines.push(String::new()); + continue; + } + if let Some(rest) = line.strip_prefix(" ") { + text_lines.push(rest.to_string()); + continue; + } + field.store(&mut spec, &text_lines); + text_field = None; + } + + // Handle capability sections + if line.starts_with("Capabilities (") { + in_capabilities_section = true; + continue; + } + // Any other top-level section header ends the capabilities section + // so that " pending_signals (0):" inside "Signal handling (1):" + // isn't mis-parsed as a capability entry. + if !line.starts_with(' ') && !line.is_empty() && line.ends_with(':') { + in_capabilities_section = false; + } + if in_capabilities_section + && line.starts_with(" ") + && line.contains(" (") + && line.ends_with("):") + { + // Start of a capability entry like " CAP_IPC_LOCK (14):" + if let Some(cap) = current_capability.take() { + spec.capabilities.push(cap); + } + + let parts: Vec<&str> = line.trim().split(" (").collect(); + if parts.len() == 2 { + let cap_name = parts[0].to_string(); + let cap_id = parts[1].trim_end_matches("):").parse().unwrap_or(0); + current_capability = Some(CapabilitySpec { + capability: cap_id, + name: cap_name, + action: String::new(), + allows: String::new(), + without_cap: String::new(), + check_condition: None, + priority: None, + alternatives: Vec::new(), + }); + parsing_capability = true; + } + continue; + } + if parsing_capability && line.starts_with(" ") { + // Parse capability fields + if let Some(ref mut cap) = current_capability { + if let Some(action) = line.strip_prefix(" Action: ") { + cap.action = action.to_string(); + } else if let Some(allows) = line.strip_prefix(" Allows: ") { + cap.allows = allows.to_string(); + } else if let Some(without) = line.strip_prefix(" Without: ") { + cap.without_cap = without.to_string(); + } else if let Some(cond) = line.strip_prefix(" Condition: ") { + cap.check_condition = Some(cond.to_string()); + } else if let Some(prio) = line.strip_prefix(" Priority: ") { + cap.priority = prio.parse().ok(); + } else if let Some(alts) = line.strip_prefix(" Alternatives: ") { + cap.alternatives = + alts.split(", ").filter_map(|s| s.parse().ok()).collect(); + } + } + continue; + } + if parsing_capability && !line.starts_with(" ") { + // End of capabilities section + if let Some(cap) = current_capability.take() { + spec.capabilities.push(cap); + } + parsing_capability = false; + } + + // Handle section headers + if line.starts_with("Parameters (") { + if let Some(count_str) = line + .strip_prefix("Parameters (") + .and_then(|s| s.strip_suffix("):")) + { + spec.param_count = count_str.parse().ok(); + } + continue; + } else if line.starts_with("Errors (") { + if let Some(count_str) = line + .strip_prefix("Errors (") + .and_then(|s| s.strip_suffix("):")) + { + spec.error_count = count_str.parse().ok(); + } + continue; + } + + // Parse regular fields + if let Some((field, first)) = TextField::start(line) { + text_field = Some(field); + text_lines.clear(); + if !first.is_empty() { + text_lines.push(first.to_string()); + } + } else if let Some(version) = line.strip_prefix("Version: ") { + spec.version = Some(version.to_string()); + } else if let Some(flags) = line.strip_prefix("Context flags: ") { + spec.context_flags = flags + .split_whitespace() + .map(|f| format!("KAPI_CTX_{f}")) + .collect(); + } else if let Some(subsys) = line.strip_prefix("Subsystem: ") { + spec.subsystem = Some(subsys.to_string()); + } else if let Some(path) = line.strip_prefix("Sysfs Path: ") { + spec.sysfs_path = Some(path.to_string()); + } else if let Some(perms) = line.strip_prefix("Permissions: ") { + spec.permissions = Some(perms.to_string()); + } + } + + if let Some(field) = text_field { + field.store(&mut spec, &text_lines); + } + + // Handle any remaining capability + if let Some(cap) = current_capability.take() { + spec.capabilities.push(cap); + } + + // Determine API type based on name + if api_name.starts_with("sys_") { + spec.api_type = "syscall".to_string(); + } else if api_name.contains("_ioctl") || api_name.starts_with("ioctl_") { + spec.api_type = "ioctl".to_string(); + } else if api_name.contains("sysfs") + || api_name.ends_with("_show") + || api_name.ends_with("_store") + { + spec.api_type = "sysfs".to_string(); + } else { + spec.api_type = "function".to_string(); + } + + Ok(spec) + } +} + +impl ApiExtractor for DebugfsExtractor { + fn extract_all(&self) -> Result<Vec<ApiSpec>> { + let api_names = self.parse_list_file()?; + let mut specs = Vec::new(); + + for name in api_names { + match self.parse_spec_file(&name) { + Ok(spec) => specs.push(spec), + Err(e) => { + eprintln!("Warning: failed to parse API spec '{}': {}", name, e); + } + } + } + + Ok(specs) + } + + fn extract_by_name(&self, name: &str) -> Result<Option<ApiSpec>> { + let api_names = 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) = self.extract_by_name(api_name)? { + display_api_spec(&spec, formatter, writer)?; + } else { + writeln!(writer, "API '{api_name}' not found in debugfs")?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Shortened sys_read dump as emitted by kapi_export_json(). + const SYS_READ_JSON: &str = r#"{ + "name": "sys_read", + "version": 1, + "description": "Read data from a file descriptor", + "long_description": "", + "context_flags": "0x21", + "parameters": [ + { + "name": "fd", + "type": "unsigned int fd", + "type_class": "file_descriptor", + "flags": "0x1", + "description": "File descriptor to read from ", + "constraint_type": "range", + "constraint_desc": "Must be a valid, open file descriptor", + "min_value": 0, + "max_value": 2147483647, + "valid_mask": "0x0", + "enum_values": [], + "size": 0, + "alignment": 0, + "size_param_idx": null, + "size_multiplier": 0 + }, + { + "name": "buf", + "type": "char __user * buf", + "type_class": "user_pointer", + "flags": "0x42", + "description": "User-space buffer to read data into ", + "constraint_type": "buffer", + "constraint_desc": "Must point to a writable user-space region", + "min_value": 0, + "max_value": 0, + "valid_mask": "0x0", + "enum_values": [], + "size": 0, + "alignment": 0, + "size_param_idx": 2, + "size_multiplier": 0 + }, + { + "name": "count", + "type": "size_t count", + "type_class": "uint", + "flags": "0x1", + "description": "Maximum number of bytes to read ", + "constraint_type": "none", + "constraint_desc": "", + "min_value": 0, + "max_value": 0, + "valid_mask": "0x0", + "enum_values": [], + "size": 0, + "alignment": 0, + "size_param_idx": null, + "size_multiplier": 0 + } + ], + "return": { + "type": "KAPI_TYPE_INT", + "type_class": "int", + "check_type": "range", + "success_value": 0, + "success_min": 0, + "success_max": 9223372036854775807, + "error_values": [], + "description": "Number of bytes read" + }, + "errors": [ + { "code": -9, "name": "EBADF", "condition": "Bad file descriptor", + "description": "fd is not valid" } + ], + "locks": [ + { "name": "file->f_pos_lock", "type": "mutex", "scope": "internal", + "description": "Position lock" } + ], + "capabilities": [ + { + "capability": 1, + "name": "CAP_DAC_OVERRIDE", + "action": "bypass_check", + "allows": "Bypass read permission checks", + "without_cap": "Standard DAC checks are enforced", + "check_condition": "", + "priority": 0 + } + ], + "constraints": [ + { "name": "MAX_RW_COUNT", + "description": "Count is clamped", "expression": "min(count, MAX_RW_COUNT)" } + ], + "signals": [ + { + "signal_num": 0, + "signal_name": "Any signal", + "direction": "0x1", + "action": 6, + "target": "", + "condition": "When blocked waiting for data", + "description": "May be interrupted", + "restartable": true, + "sa_flags_required": "0x0", + "sa_flags_forbidden": "0x0", + "error_on_signal": -4, + "transform_to": 0, + "timing": "during", + "priority": 0, + "interruptible": false, + "queue_behavior": "", + "state_required": "0x0", + "state_forbidden": "0x0" + } + ], + "side_effects": [ + { "type": "0x10", "target": "file->f_pos", "condition": "", + "description": "Offset advances", "reversible": false } + ], + "state_transitions": [], + "signal_masks": [], + "struct_specs": [], + "examples": "", + "notes": "" +} +"#; + + // Synthetic dump exercising the fields sys_read leaves empty: enum + // values, error values, struct specs, signal masks, state transitions. + const FULL_JSON: &str = r#"{ + "name": "tmp_full", + "version": 1, + "description": "Full \"feature\" test\tline\nbreak\u0001", + "long_description": "", + "context_flags": "0x91", + "parameters": [ + { + "name": "mode", + "type": "int mode", + "type_class": "enum", + "flags": "0x1", + "description": "Enumerated parameter", + "constraint_type": "enum", + "constraint_desc": "", + "min_value": 0, + "max_value": 0, + "valid_mask": "0x0", + "enum_values": [1, 2, -3, 1000000000000], + "size": 4096, + "alignment": 64, + "size_param_idx": null, + "size_multiplier": 0 + }, + { + "name": "bits", + "type": "u64 bits", + "type_class": "uint", + "flags": "0x1", + "description": "Masked parameter", + "constraint_type": "mask", + "constraint_desc": "", + "min_value": 0, + "max_value": 0, + "valid_mask": "0xffffffffffffffff", + "enum_values": [], + "size": 0, + "alignment": 0, + "size_param_idx": null, + "size_multiplier": 0 + } + ], + "return": { + "type": "long", + "type_class": "int", + "check_type": "error_check", + "success_value": 0, + "success_min": 0, + "success_max": 0, + "error_values": [-1, -22, -4095], + "description": "Error-checked return" + }, + "errors": [], + "locks": [ + { "name": "tmp_lock", "type": "seqlock", "scope": "acquires", "description": "a seqlock" } + ], + "capabilities": [ + { + "capability": 21, + "name": "CAP_SYS_ADMIN", + "action": "perform_operation", + "allows": "do it", + "without_cap": "cannot", + "check_condition": "always checked", + "priority": 3, + "alternatives": [21, 17] + } + ], + "constraints": [], + "signals": [], + "side_effects": [], + "state_transitions": [ + { "object": "obj", "from_state": "a", "to_state": "b", "condition": "when asked", + "description": "moves a to b" } + ], + "signal_masks": [ + { "name": "blocked", "description": "Blocked while running", "signals": [2, 15, 3] } + ], + "struct_specs": [ + { + "name": "tmp_struct", + "size": 16, + "alignment": 8, + "description": "Test structure", + "fields": [ + { + "name": "flags", + "type": "u32", + "type_class": "uint", + "offset": 0, + "size": 4, + "flags": "0x1", + "constraint_type": "mask", + "min_value": 0, + "max_value": 0, + "valid_mask": "0xff", + "enum_values": "", + "description": "Flag bits" + }, + { + "name": "kind", + "type": "enum k", + "type_class": "enum", + "offset": 8, + "size": 4, + "flags": "0x0", + "constraint_type": "range", + "min_value": -5, + "max_value": 5, + "valid_mask": "0x0", + "enum_values": "", + "description": "Kind" + } + ] + } + ], + "examples": "", + "notes": "note" +} +"#; + + // Dump without the optional per-parameter constraint fields. + const MINIMAL_JSON: &str = r#"{ + "name": "sys_read", + "version": 1, + "description": "Read data from a file descriptor", + "long_description": "Reads.", + "context_flags": "0x21", + "parameters": [ + { + "name": "buf", + "type": "char __user * buf", + "type_class": "user_pointer", + "flags": "0x42", + "description": "User-space buffer to read data into " + } + ], + "return": { + "type": "KAPI_TYPE_INT", + "type_class": "int", + "check_type": "range", + "success_min": 0, + "success_max": 9223372036854775807, + "description": "Bytes read" + }, + "errors": [], + "locks": [], + "capabilities": [ + { + "capability": 1, + "name": "CAP_DAC_OVERRIDE", + "action": "bypass_check", + "allows": "a", + "without_cap": "b", + "check_condition": "c", + "priority": 0 + } + ], + "constraints": [], + "signals": [], + "side_effects": [], + "examples": "", + "notes": "" +} +"#; + + fn extractor() -> DebugfsExtractor { + DebugfsExtractor { + debugfs_path: PathBuf::new(), + } + } + + #[test] + fn json_sys_read_carries_type_and_constraint_data() { + let spec = extractor().try_parse_json(SYS_READ_JSON).unwrap(); + + assert_eq!(spec.api_type, "syscall"); + assert_eq!(spec.version.as_deref(), Some("1")); + assert_eq!(spec.long_description, None); + assert_eq!( + spec.context_flags, + ["KAPI_CTX_PROCESS", "KAPI_CTX_SLEEPABLE"] + ); + assert_eq!(spec.param_count, Some(3)); + + let fd = &spec.parameters[0]; + assert_eq!((fd.param_type, fd.constraint_type), (9, 1)); + assert_eq!((fd.min_value, fd.max_value), (Some(0), Some(2147483647))); + assert_eq!(fd.valid_mask, None); + assert_eq!(fd.size_param_idx, None); + assert_eq!( + fd.constraint.as_deref(), + Some("Must be a valid, open file descriptor") + ); + + let buf = &spec.parameters[1]; + assert_eq!((buf.param_type, buf.constraint_type), (10, 11)); + assert_eq!(buf.flags, 0x42); + assert_eq!(buf.size_param_idx, Some(2)); + assert_eq!( + (buf.min_value, buf.max_value, buf.valid_mask), + (None, None, None) + ); + assert_eq!((buf.size, buf.alignment), (None, None)); + assert_eq!( + spec.parameters[buf.size_param_idx.unwrap() as usize].name, + "count" + ); + + let count = &spec.parameters[2]; + assert_eq!((count.param_type, count.constraint_type), (2, 0)); + assert_eq!(count.constraint, None); + assert_eq!(count.description, "Maximum number of bytes to read "); + + let ret = spec.return_spec.as_ref().unwrap(); + assert_eq!((ret.return_type, ret.check_type), (1, 1)); + assert_eq!(ret.success_value, None); + assert_eq!(ret.success_min, Some(0)); + assert_eq!(ret.success_max, Some(i64::MAX)); + assert!(ret.error_values.is_empty()); + + assert_eq!(spec.errors[0].error_code, -9); + assert_eq!((spec.locks[0].lock_type, spec.locks[0].scope), (1, 0)); + assert_eq!(spec.capabilities[0].action, "KAPI_CAP_BYPASS_CHECK"); + assert_eq!(spec.capabilities[0].check_condition, None); + assert_eq!(spec.signals[0].timing, 1); + assert_eq!(spec.signals[0].error_on_signal, Some(-4)); + assert_eq!(spec.signals[0].target, None); + assert_eq!(spec.side_effects[0].effect_type, 0x10); + assert_eq!(spec.side_effects[0].condition, None); + assert_eq!(spec.constraints.len(), 1); + } + + #[test] + fn json_full_carries_enum_error_struct_and_transition_data() { + let spec = extractor().try_parse_json(FULL_JSON).unwrap(); + + assert_eq!( + spec.description.as_deref(), + Some("Full \"feature\" test\tline\nbreak\u{1}") + ); + assert_eq!( + spec.context_flags, + [ + "KAPI_CTX_PROCESS", + "KAPI_CTX_ATOMIC", + "KAPI_CTX_IRQ_DISABLED" + ] + ); + + let mode = &spec.parameters[0]; + assert_eq!((mode.param_type, mode.constraint_type), (6, 3)); + assert_eq!(mode.enum_values, ["1", "2", "-3", "1000000000000"]); + assert_eq!((mode.size, mode.alignment), (Some(4096), Some(64))); + assert_eq!( + (mode.min_value, mode.max_value, mode.valid_mask), + (None, None, None) + ); + + let bits = &spec.parameters[1]; + assert_eq!(bits.constraint_type, 2); + assert_eq!(bits.valid_mask, Some(u64::MAX)); + assert_eq!((bits.min_value, bits.max_value), (None, None)); + assert_eq!((bits.size, bits.alignment), (None, None)); + + let ret = spec.return_spec.as_ref().unwrap(); + assert_eq!(ret.check_type, 2); + assert_eq!(ret.error_values, [-1, -22, -4095]); + + assert_eq!(spec.capabilities[0].alternatives, [21, 17]); + assert_eq!(spec.capabilities[0].priority, Some(3)); + assert_eq!((spec.locks[0].lock_type, spec.locks[0].scope), (4, 1)); + + let trans = &spec.state_transitions[0]; + assert_eq!( + (trans.object.as_str(), trans.from_state.as_str()), + ("obj", "a") + ); + assert_eq!(trans.to_state, "b"); + assert_eq!(trans.condition.as_deref(), Some("when asked")); + + assert_eq!(spec.signal_masks[0].name, "blocked"); + assert_eq!(spec.signal_masks[0].signals, [2, 15, 3]); + + let st = &spec.struct_specs[0]; + assert_eq!( + (st.name.as_str(), st.size, st.alignment), + ("tmp_struct", 16, 8) + ); + assert_eq!(st.field_count, 2); + assert_eq!(st.description, "Test structure"); + assert_eq!( + (st.fields[0].field_type, st.fields[0].constraint_type), + (2, 2) + ); + assert_eq!(st.fields[0].valid_mask, 0xff); + assert_eq!(st.fields[1].offset, 8); + assert_eq!((st.fields[1].min_value, st.fields[1].max_value), (-5, 5)); + } + + #[test] + fn json_without_constraint_fields_still_parses() { + let spec = extractor().try_parse_json(MINIMAL_JSON).unwrap(); + + let buf = &spec.parameters[0]; + assert_eq!(buf.param_type, 10); + assert_eq!(buf.constraint_type, 0); + assert_eq!(buf.min_value, None); + assert_eq!(buf.valid_mask, None); + assert_eq!(buf.size_param_idx, None); + assert!(buf.enum_values.is_empty()); + + let ret = spec.return_spec.as_ref().unwrap(); + assert_eq!((ret.return_type, ret.check_type), (1, 1)); + assert_eq!(ret.success_value, None); + assert_eq!(ret.success_max, Some(i64::MAX)); + assert_eq!(spec.capabilities[0].action, "KAPI_CAP_BYPASS_CHECK"); + assert!(spec.state_transitions.is_empty()); + assert!(spec.struct_specs.is_empty()); + } + + #[test] + fn enum_tokens_match_kernel_numbering() { + let classes = [ + "void", + "int", + "uint", + "pointer", + "struct", + "union", + "enum", + "function_pointer", + "array", + "file_descriptor", + "user_pointer", + "pathname", + "custom", + ]; + for (n, token) in classes.iter().enumerate() { + assert_eq!( + DebugfsExtractor::parse_type_class(token), + n as u32, + "{token}" + ); + } + + let constraints = [ + "none", + "range", + "mask", + "enum", + "alignment", + "power_of_two", + "page_aligned", + "nonzero", + "user_string", + "user_path", + "user_ptr", + "buffer", + "custom", + ]; + for (n, token) in constraints.iter().enumerate() { + assert_eq!( + DebugfsExtractor::parse_constraint_type(token), + n as u32, + "{token}" + ); + } + } + + #[test] + fn api_type_follows_symbol_name() { + assert_eq!(DebugfsExtractor::api_type_from_name("sys_read"), "syscall"); + assert_eq!(DebugfsExtractor::api_type_from_name("foo_ioctl"), "ioctl"); + assert_eq!(DebugfsExtractor::api_type_from_name("kmalloc"), "function"); + } + + #[test] + fn truncated_json_is_rejected() { + let cut = &SYS_READ_JSON[..SYS_READ_JSON.len() / 2]; + assert!(extractor().try_parse_json(cut).is_err()); + } + + fn debugfs_with(json: &str, text: &str) -> tempfile::TempDir { + let dir = tempfile::tempdir().unwrap(); + let kapi = dir.path().join("kapi"); + fs::create_dir_all(kapi.join("specs")).unwrap(); + fs::create_dir_all(kapi.join("specs-json")).unwrap(); + fs::write( + kapi.join("list"), + "Available Kernel API Specifications\n\ + ===================================\n\n\ + sys_read - Read data from a file descriptor\n\n\ + Total: 1 specifications\n", + ) + .unwrap(); + fs::write(kapi.join("specs-json/sys_read"), json).unwrap(); + fs::write(kapi.join("specs/sys_read"), text).unwrap(); + dir + } + + #[test] + fn reads_json_endpoint_from_debugfs_tree() { + let dir = debugfs_with(SYS_READ_JSON, "Name: sys_read\n"); + let ex = DebugfsExtractor::new(Some(dir.path().to_string_lossy().into_owned())).unwrap(); + let spec = ex.extract_by_name("sys_read").unwrap().unwrap(); + + assert_eq!(spec.parameters[1].constraint_type, 11); + assert_eq!(spec.parameters[1].size_param_idx, Some(2)); + } + + #[test] + fn truncated_json_endpoint_falls_back_to_text_dump() { + let text = "Name: sys_read\n\ + Version: 1\n\ + Description: Read data from a file descriptor\n\ + Context flags: PROCESS SLEEPABLE \n"; + let dir = debugfs_with(&SYS_READ_JSON[..SYS_READ_JSON.len() / 2], text); + let ex = DebugfsExtractor::new(Some(dir.path().to_string_lossy().into_owned())).unwrap(); + let spec = ex.extract_by_name("sys_read").unwrap().unwrap(); + + assert!(spec.parameters.is_empty()); + assert_eq!( + spec.context_flags, + ["KAPI_CTX_PROCESS", "KAPI_CTX_SLEEPABLE"] + ); + } + + #[test] + fn text_dump_multi_line_values_stay_in_their_field() { + let text = "Kernel API Specification\n\ + ========================\n\n\ + Name: sys_read\n\ + Version: 1\n\ + Description: Read data from a file descriptor\n\ + Long description:\n \ + First paragraph.\n\n \ + Notes: not a header\n \ + Version: 9\n\ + Context flags: PROCESS SLEEPABLE \n\n\ + Parameters (1):\n \ + [0] fd:\n \ + description: File descriptor\n\n\ + Examples:\n \ + read(fd, buf, n);\n \ + close(fd);\n\n\ + Notes:\n \ + one\n\n \ + two\n\n"; + let dir = debugfs_with("", text); + let ex = DebugfsExtractor::new(Some(dir.path().to_string_lossy().into_owned())).unwrap(); + let spec = ex.extract_by_name("sys_read").unwrap().unwrap(); + + assert_eq!(spec.version.as_deref(), Some("1")); + assert_eq!( + spec.description.as_deref(), + Some("Read data from a file descriptor") + ); + assert_eq!( + spec.long_description.as_deref(), + Some("First paragraph.\n\nNotes: not a header\nVersion: 9") + ); + assert_eq!( + spec.context_flags, + ["KAPI_CTX_PROCESS", "KAPI_CTX_SLEEPABLE"] + ); + assert_eq!(spec.param_count, Some(1)); + assert_eq!( + spec.examples.as_deref(), + Some("read(fd, buf, n);\nclose(fd);") + ); + assert_eq!(spec.notes.as_deref(), Some("one\n\ntwo")); + } +} |
