// SPDX-License-Identifier: MIT // // Copyright IBM Corp. 2025 //! I/O utilities for the PV Info Tool use std::collections::HashMap; use std::path::Path; use std::str; use anyhow::{Context, Result}; use pv_core::misc::{read_file, read_file_string, try_parse_u64, Flags, Msb0Flags64}; use crate::constants::*; // Verify that the Ultravisor directory exists pub fn check_uv_exists() -> Result<()> { let uv_path = Path::new(BASE_DIR); if !uv_path.exists() { return Err(std::io::Error::new( std::io::ErrorKind::NotFound, "Ultravisor directory missing", )) .context("This system is neither a SE-host or SE-guest"); } Ok(()) } // File readers pub fn read_hex_from_file>(path: P, context: &str) -> Option { // Add precise context for which file failed let content_str = read_file_string(path.as_ref(), context).ok()?; let first_line = content_str.lines().next()?.trim(); if first_line.is_empty() { return None; } match try_parse_u64(first_line, context) { Ok(val) if val != 0 => Some(val), Ok(_) => None, // treat "0x0" as absent Err(e) => panic!( "Failed to parse hex value in {} ({}): {}", path.as_ref().display(), context, e ), } } pub fn read_integer_from_file>(path: P, context: &str) -> Option { let content_str = read_file_string(path.as_ref(), context).ok()?; let first_line = content_str.lines().next()?.trim(); if first_line.is_empty() { return None; } match first_line.parse::() { Ok(val) => Some(val), Err(e) => panic!( "Failed to parse integer in {} ({}): {}", path.as_ref().display(), context, e ), } } pub fn read_bool_from_file>(path: P, context: &str) -> bool { let content = read_file(path.as_ref(), context).unwrap_or_else(|e| { panic!( "Failed to read {} ({}): {}", path.as_ref().display(), context, e ) }); let content_str = str::from_utf8(&content) .unwrap_or_else(|_| panic!("Invalid UTF-8 in {} ({})", path.as_ref().display(), context)); match content_str.lines().next().unwrap_or("").trim() { "1" => true, "0" => false, other => panic!( "Invalid boolean value in {} ({}): {}", path.as_ref().display(), context, other ), } } // Collectors pub fn collect_bit_messages(hex_value: u64, desc_content: &str) -> Vec { let flags = Msb0Flags64::from(hex_value); let mut messages = Vec::new(); for (line_index, line) in desc_content.lines().enumerate() { if flags.is_set(line_index as u8) { if line.to_lowercase().contains("reserved") { messages.push(format!("{line} Bit-{line_index}")); } else { messages.push(line.to_string()); } } } if messages.is_empty() { messages.push("No matching messages. But these bits are ON:".into()); for bit in 0..64 { if flags.is_set(bit) { messages.push(format!("- Bit {bit} is ON")); } } } messages } pub fn collect_version_flags(hex_value: u64) -> Vec { let flags = Msb0Flags64::from(hex_value); let mut versions = Vec::new(); for bit in 0..64 { if flags.is_set(bit as u8) { let version = (bit + 1) * 0x100; versions.push(format!("version {version:x} hex is supported")); } } versions } pub fn collect_limits(query_dir: &Path) -> HashMap { let mut map = HashMap::new(); for (file, desc) in LIMITS { if let Some(val) = read_integer_from_file(query_dir.join(file), file) { map.insert(desc.to_string(), val); } } map } #[cfg(test)] mod test { //! Unit Tests for io_utils use std::io::{self, Write}; use std::path::Path; use tempfile::{tempdir, NamedTempFile}; use super::*; // Tests check_uv_exists() by observing the real BASE_DIR at test runtime // if BASE_DIR exists on the machine running the tests, check_uv_exists() must return Ok // otherwise, it must return Err(anyhow::Error) whose cause is an io::ErrorKind::NotFound, // and whose message also contains the added context string #[test] fn test_check_uv_exists_behaviour_matches_base_dir() { let base_dir = BASE_DIR; let uv_path = Path::new(base_dir); let res = check_uv_exists(); if uv_path.exists() { assert!( res.is_ok(), "BASE_DIR exists but check_uv_exists returned Err: {res:?}" ); } else { assert!( res.is_err(), "BASE_DIR missing but check_uv_exists returned Ok" ); if let Err(e) = res { // downcast anyhow::Error into std::io::Error if let Some(io_err) = e.downcast_ref::() { assert_eq!(io_err.kind(), io::ErrorKind::NotFound); } else { panic!("Expected std::io::Error inside anyhow::Error, got: {e:?}"); } // ensure both the base error and context message appear let msg = format!("{e}"); assert!(msg.contains("Ultravisor directory missing")); assert!(msg.contains("This system is neither a SE-host or SE-guest")); } } } // read_hex_from_file tests #[test] fn test_read_hex_from_file_valid() { let mut file = NamedTempFile::new().unwrap(); writeln!(file, "0x1a2b").unwrap(); let got = read_hex_from_file(file.path(), "test"); assert_eq!(got, Some(0x1a2b)); } #[test] fn test_read_hex_from_file_zero_is_none() { let mut file = NamedTempFile::new().unwrap(); writeln!(file, "0x0").unwrap(); assert_eq!(read_hex_from_file(file.path(), "test"), None); } #[test] #[should_panic(expected = "Failed to parse hex value")] fn test_read_hex_from_file_invalid_panics() { let mut file = NamedTempFile::new().unwrap(); writeln!(file, "not_hex").unwrap(); // should panic now let _ = read_hex_from_file(file.path(), "test"); } #[test] fn test_read_hex_from_file_missing_returns_none() { // missing file -> None let dir = tempdir().unwrap(); let missing = dir.path().join("no_such_file"); assert_eq!(read_hex_from_file(missing, "test"), None); } // read_integer_from_file tests #[test] fn test_read_integer_from_file_valid() { let mut file = NamedTempFile::new().unwrap(); writeln!(file, "12345").unwrap(); assert_eq!(read_integer_from_file(file.path(), "test"), Some(12345)); } #[test] fn test_read_integer_from_file_empty_returns_none() { let mut file = NamedTempFile::new().unwrap(); writeln!(file).unwrap(); assert_eq!(read_integer_from_file(file.path(), "test"), None); } #[test] #[should_panic(expected = "Failed to parse integer")] fn test_read_integer_from_file_invalid_panics() { let mut file2 = NamedTempFile::new().unwrap(); writeln!(file2, "abc").unwrap(); // should panic now let _ = read_integer_from_file(file2.path(), "test"); } // read_bool_from_file tests #[test] fn test_read_bool_from_file_true() { let mut file = NamedTempFile::new().unwrap(); writeln!(file, "1").unwrap(); assert!(read_bool_from_file(file.path(), "test")); } #[test] fn test_read_bool_from_file_false() { let mut file = NamedTempFile::new().unwrap(); writeln!(file, "0").unwrap(); assert!(!read_bool_from_file(file.path(), "test")); } #[test] #[should_panic(expected = "Invalid boolean value")] fn test_read_bool_from_file_invalid_value_panics() { let mut file = NamedTempFile::new().unwrap(); writeln!(file, "yes").unwrap(); // should panic with clear error let _ = read_bool_from_file(file.path(), "test"); } // collect_bit_messages tests #[test] fn test_collect_bit_messages_matches_and_reserved() { // lines: index 0 -> bit 63, index 1 -> bit 62, index 2 -> bit 61 let desc = "First Feature\nreserved for future use\nThird Feature"; // set bit for line 0 and line 2 let hex = (1u64 << 63) | (1u64 << 61); let messages = collect_bit_messages(hex, desc); // Expect "First Feature" and "Third Feature" (and the reserved line should get the " // Bit-" if matched) assert!(messages.iter().any(|m| m == "First Feature")); assert!(messages.iter().any(|m| m == "Third Feature")); // reserved wasn't set here; now test reserved specifically below } #[test] fn test_collect_bit_messages_reserved_line() { let desc = "one\nReserved entry\nthree"; // set the bit corresponding to line index 1 -> bit 62 let hex = 1u64 << (63 - 1); let messages = collect_bit_messages(hex, desc); assert_eq!(messages.len(), 1); assert_eq!(messages[0], "Reserved entry Bit-1"); } #[test] fn test_collect_bit_messages_fallback_lists_bits() { let desc = "only one line"; // pick a bit that does not map to line 0 (63) so fallback triggers; e.g. bit 5 let hex = 1u64 << 5; let messages = collect_bit_messages(hex, desc); assert!(!messages.is_empty()); assert!(messages[0].starts_with("No matching messages")); assert!(messages.iter().any(|m| m.contains("Bit 5"))); } // collect_version_flags tests #[test] fn test_collect_version_flags_two_bits() { // top two bits set => versions 0x100 and 0x200 should be present let hex = (1u64 << 63) | (1u64 << 62); let versions = collect_version_flags(hex); assert!(versions.contains(&"version 100 hex is supported".to_string())); assert!(versions.contains(&"version 200 hex is supported".to_string())); } }