Prepare public release v0.1.0

This commit is contained in:
2026-01-30 09:44:12 +01:00
parent 75be95fdf7
commit 2034281ad7
41 changed files with 2137 additions and 1028 deletions

View File

@@ -117,9 +117,7 @@ fn get_real_username() -> String {
std::process::Command::new("whoami")
.output()
.ok()
.and_then(|output| {
String::from_utf8(output.stdout).ok()
})
.and_then(|output| String::from_utf8(output.stdout).ok())
.map(|s| s.trim().to_string())
.unwrap_or_else(|| "unknown".to_string())
})
@@ -130,7 +128,11 @@ async fn main() -> Result<()> {
let cli = Cli::parse();
// Initialize logging
let log_level = if cli.verbose { Level::DEBUG } else { Level::INFO };
let log_level = if cli.verbose {
Level::DEBUG
} else {
Level::INFO
};
FmtSubscriber::builder()
.with_max_level(log_level)
.with_target(false)
@@ -140,30 +142,22 @@ async fn main() -> Result<()> {
let config = Config::load_or_default();
match cli.command {
Commands::Capture { output, count, device } => {
cmd_capture(&config, &output, count, device.as_deref()).await
}
Commands::Detect { image, scores, output } => {
cmd_detect(&config, image.as_deref(), scores, output.as_deref()).await
}
Commands::Status { camera, daemon } => {
cmd_status(&config, camera, daemon).await
}
Commands::Enroll { label } => {
cmd_enroll(&config, &label).await
}
Commands::List => {
cmd_list(&config).await
}
Commands::Remove { label, all } => {
cmd_remove(&config, label.as_deref(), all).await
}
Commands::Test { verbose, debug } => {
cmd_test(&config, verbose, debug).await
}
Commands::Config { json, set } => {
cmd_config(&config, json, set.as_deref()).await
}
Commands::Capture {
output,
count,
device,
} => cmd_capture(&config, &output, count, device.as_deref()).await,
Commands::Detect {
image,
scores,
output,
} => cmd_detect(&config, image.as_deref(), scores, output.as_deref()).await,
Commands::Status { camera, daemon } => cmd_status(&config, camera, daemon).await,
Commands::Enroll { label } => cmd_enroll(&config, &label).await,
Commands::List => cmd_list(&config).await,
Commands::Remove { label, all } => cmd_remove(&config, label.as_deref(), all).await,
Commands::Test { verbose, debug } => cmd_test(&config, verbose, debug).await,
Commands::Config { json, set } => cmd_config(&config, json, set.as_deref()).await,
}
}
@@ -211,12 +205,18 @@ async fn cmd_capture(
for i in 0..count {
let frame = camera.capture_frame()?;
match frame.format {
PixelFormat::Grey => {
let filename = format!("{}/frame_{:03}.png", output, i);
if let Some(img) = image::GrayImage::from_raw(frame.width, frame.height, frame.data) {
img.save(&filename).map_err(|e| linux_hello_common::Error::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
if let Some(img) = image::GrayImage::from_raw(frame.width, frame.height, frame.data)
{
img.save(&filename).map_err(|e| {
linux_hello_common::Error::Io(std::io::Error::new(
std::io::ErrorKind::Other,
e,
))
})?;
info!("Saved frame {} (Permissions: Grey)", filename);
}
}
@@ -227,9 +227,18 @@ async fn cmd_capture(
for chunk in frame.data.chunks_exact(2) {
gray_data.push(chunk[0]); // Y component
}
if let Some(img) = image::GrayImage::from_raw(frame.width, frame.height, gray_data) {
img.save(&filename).map_err(|e| linux_hello_common::Error::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
info!("Saved frame {} (converted from YUYV to grayscale)", filename);
if let Some(img) = image::GrayImage::from_raw(frame.width, frame.height, gray_data)
{
img.save(&filename).map_err(|e| {
linux_hello_common::Error::Io(std::io::Error::new(
std::io::ErrorKind::Other,
e,
))
})?;
info!(
"Saved frame {} (converted from YUYV to grayscale)",
filename
);
}
}
PixelFormat::Mjpeg => {
@@ -238,7 +247,12 @@ async fn cmd_capture(
match image::load_from_memory(&frame.data) {
Ok(img) => {
let gray = img.to_luma8();
gray.save(&filename).map_err(|e| linux_hello_common::Error::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
gray.save(&filename).map_err(|e| {
linux_hello_common::Error::Io(std::io::Error::new(
std::io::ErrorKind::Other,
e,
))
})?;
info!("Saved frame {} (decoded from MJPEG)", filename);
}
Err(e) => {
@@ -249,7 +263,11 @@ async fn cmd_capture(
_ => {
let filename = format!("{}/frame_{:03}.raw", output, i);
std::fs::write(&filename, &frame.data)?;
info!("Saved frame {} ({} bytes - Unknown format)", filename, frame.data.len());
info!(
"Saved frame {} ({} bytes - Unknown format)",
filename,
frame.data.len()
);
}
}
}
@@ -294,26 +312,30 @@ async fn cmd_detect(
info!("Saving annotated image to: {}", out_path);
// Convert to RGB for drawing
let mut rgb_img = img.to_rgb8();
// Draw bounding box (red color)
draw_bounding_box(&mut rgb_img, x, y, w, h, [255, 0, 0]);
rgb_img.save(out_path)
.map_err(|e| linux_hello_common::Error::Io(
std::io::Error::new(std::io::ErrorKind::Other, e)
))?;
rgb_img.save(out_path).map_err(|e| {
linux_hello_common::Error::Io(std::io::Error::new(
std::io::ErrorKind::Other,
e,
))
})?;
println!("Annotated image saved to: {}", out_path);
}
}
None => {
println!("No face detected");
if let Some(out_path) = output {
// Save original image without annotations
img.save(out_path)
.map_err(|e| linux_hello_common::Error::Io(
std::io::Error::new(std::io::ErrorKind::Other, e)
))?;
img.save(out_path).map_err(|e| {
linux_hello_common::Error::Io(std::io::Error::new(
std::io::ErrorKind::Other,
e,
))
})?;
println!("Image saved to: {} (no face detected)", out_path);
}
}
@@ -329,23 +351,16 @@ async fn cmd_detect(
}
/// Draw a bounding box on an RGB image
fn draw_bounding_box(
img: &mut image::RgbImage,
x: u32,
y: u32,
w: u32,
h: u32,
color: [u8; 3],
) {
fn draw_bounding_box(img: &mut image::RgbImage, x: u32, y: u32, w: u32, h: u32, color: [u8; 3]) {
let (img_width, img_height) = img.dimensions();
let thickness = 2u32;
// Clamp coordinates to image bounds
let x1 = x.min(img_width.saturating_sub(1));
let y1 = y.min(img_height.saturating_sub(1));
let x2 = (x + w).min(img_width.saturating_sub(1));
let y2 = (y + h).min(img_height.saturating_sub(1));
// Draw horizontal lines (top and bottom)
for t in 0..thickness {
// Top line
@@ -353,7 +368,7 @@ fn draw_bounding_box(
for px in x1..=x2 {
img.put_pixel(px, top_y, image::Rgb(color));
}
// Bottom line
let bottom_y = y2.saturating_sub(t);
if bottom_y >= y1 {
@@ -362,7 +377,7 @@ fn draw_bounding_box(
}
}
}
// Draw vertical lines (left and right)
for t in 0..thickness {
// Left line
@@ -370,7 +385,7 @@ fn draw_bounding_box(
for py in y1..=y2 {
img.put_pixel(left_x, py, image::Rgb(color));
}
// Right line
let right_x = x2.saturating_sub(t);
if right_x >= x1 {
@@ -425,28 +440,42 @@ async fn cmd_status(config: &Config, show_camera: bool, show_daemon: bool) -> Re
println!("\nConfiguration:");
println!(" Detection model: {}", config.detection.model);
println!(" Anti-spoofing: {}", if config.anti_spoofing.enabled { "enabled" } else { "disabled" });
println!(" TPM: {}", if config.tpm.enabled { "enabled" } else { "disabled" });
println!(
" Anti-spoofing: {}",
if config.anti_spoofing.enabled {
"enabled"
} else {
"disabled"
}
);
println!(
" TPM: {}",
if config.tpm.enabled {
"enabled"
} else {
"disabled"
}
);
Ok(())
}
async fn cmd_enroll(config: &Config, label: &str) -> Result<()> {
use linux_hello_daemon::auth::AuthService;
info!("Starting enrollment with label: {}", label);
// Get real user (handles sudo)
let user = get_real_username();
println!("Enrolling user: {}", user);
println!("Label: {}", label);
println!("Please look at the camera...");
// Create auth service
let auth_service = AuthService::new(config.clone());
auth_service.initialize()?;
// Enroll with 5 frames
match auth_service.enroll(&user, label, 5).await {
Ok(()) => {
@@ -462,16 +491,16 @@ async fn cmd_enroll(config: &Config, label: &str) -> Result<()> {
async fn cmd_list(_config: &Config) -> Result<()> {
use linux_hello_common::TemplateStore;
let store = TemplateStore::new(TemplateStore::default_path());
let users = store.list_users()?;
if users.is_empty() {
println!("No enrolled users");
return Ok(());
}
println!("Enrolled users:");
for user in users {
let templates = store.list_templates(&user)?;
@@ -481,28 +510,27 @@ async fn cmd_list(_config: &Config) -> Result<()> {
println!(" {}:", user);
for label in templates {
if let Ok(template) = store.load(&user, &label) {
println!(" - {} (enrolled: {}, frames: {})",
label,
template.enrolled_at,
template.frame_count
println!(
" - {} (enrolled: {}, frames: {})",
label, template.enrolled_at, template.frame_count
);
}
}
}
}
Ok(())
}
async fn cmd_remove(_config: &Config, label: Option<&str>, all: bool) -> Result<()> {
use linux_hello_common::TemplateStore;
let user = std::env::var("USER")
.or_else(|_| std::env::var("USERNAME"))
.unwrap_or_else(|_| "unknown".to_string());
let store = TemplateStore::new(TemplateStore::default_path());
if all {
store.remove_all(&user)?;
println!("Removed all templates for user: {}", user);
@@ -515,23 +543,23 @@ async fn cmd_remove(_config: &Config, label: Option<&str>, all: bool) -> Result<
"No label specified".to_string(),
));
}
Ok(())
}
async fn cmd_test(config: &Config, verbose: bool, _debug: bool) -> Result<()> {
use linux_hello_daemon::auth::AuthService;
// Get real user (handles sudo)
let user = get_real_username();
println!("Testing authentication for user: {}", user);
println!("Please look at the camera...");
// Create auth service
let auth_service = AuthService::new(config.clone());
auth_service.initialize()?;
match auth_service.authenticate(&user).await {
Ok(true) => {
println!("✓ Authentication successful!");
@@ -560,62 +588,77 @@ async fn cmd_config(config: &Config, json: bool, set: Option<&str>) -> Result<()
"Invalid format. Use key=value".to_string(),
));
}
let key = parts[0].trim();
let value = parts[1].trim();
let mut new_config = config.clone();
// Update configuration based on key
match key {
"general.log_level" => new_config.general.log_level = value.to_string(),
"general.timeout_seconds" => {
new_config.general.timeout_seconds = value.parse()
.map_err(|_| linux_hello_common::Error::Config("Invalid timeout value".to_string()))?;
new_config.general.timeout_seconds = value.parse().map_err(|_| {
linux_hello_common::Error::Config("Invalid timeout value".to_string())
})?;
}
"camera.device" => new_config.camera.device = value.to_string(),
"camera.ir_emitter" => new_config.camera.ir_emitter = value.to_string(),
"camera.fps" => {
new_config.camera.fps = value.parse()
.map_err(|_| linux_hello_common::Error::Config("Invalid fps value".to_string()))?;
new_config.camera.fps = value.parse().map_err(|_| {
linux_hello_common::Error::Config("Invalid fps value".to_string())
})?;
}
"detection.model" => new_config.detection.model = value.to_string(),
"detection.min_face_size" => {
new_config.detection.min_face_size = value.parse()
.map_err(|_| linux_hello_common::Error::Config("Invalid min_face_size value".to_string()))?;
new_config.detection.min_face_size = value.parse().map_err(|_| {
linux_hello_common::Error::Config("Invalid min_face_size value".to_string())
})?;
}
"detection.confidence_threshold" => {
new_config.detection.confidence_threshold = value.parse()
.map_err(|_| linux_hello_common::Error::Config("Invalid confidence_threshold value".to_string()))?;
new_config.detection.confidence_threshold = value.parse().map_err(|_| {
linux_hello_common::Error::Config(
"Invalid confidence_threshold value".to_string(),
)
})?;
}
"embedding.model" => new_config.embedding.model = value.to_string(),
"embedding.distance_threshold" => {
new_config.embedding.distance_threshold = value.parse()
.map_err(|_| linux_hello_common::Error::Config("Invalid distance_threshold value".to_string()))?;
new_config.embedding.distance_threshold = value.parse().map_err(|_| {
linux_hello_common::Error::Config(
"Invalid distance_threshold value".to_string(),
)
})?;
}
"anti_spoofing.enabled" => {
new_config.anti_spoofing.enabled = value.parse()
.map_err(|_| linux_hello_common::Error::Config("Invalid boolean value".to_string()))?;
new_config.anti_spoofing.enabled = value.parse().map_err(|_| {
linux_hello_common::Error::Config("Invalid boolean value".to_string())
})?;
}
"anti_spoofing.depth_check" => {
new_config.anti_spoofing.depth_check = value.parse()
.map_err(|_| linux_hello_common::Error::Config("Invalid boolean value".to_string()))?;
new_config.anti_spoofing.depth_check = value.parse().map_err(|_| {
linux_hello_common::Error::Config("Invalid boolean value".to_string())
})?;
}
"anti_spoofing.liveness_model" => {
new_config.anti_spoofing.liveness_model = value.parse()
.map_err(|_| linux_hello_common::Error::Config("Invalid boolean value".to_string()))?;
new_config.anti_spoofing.liveness_model = value.parse().map_err(|_| {
linux_hello_common::Error::Config("Invalid boolean value".to_string())
})?;
}
"anti_spoofing.min_score" => {
new_config.anti_spoofing.min_score = value.parse()
.map_err(|_| linux_hello_common::Error::Config("Invalid min_score value".to_string()))?;
new_config.anti_spoofing.min_score = value.parse().map_err(|_| {
linux_hello_common::Error::Config("Invalid min_score value".to_string())
})?;
}
"tpm.enabled" => {
new_config.tpm.enabled = value.parse()
.map_err(|_| linux_hello_common::Error::Config("Invalid boolean value".to_string()))?;
new_config.tpm.enabled = value.parse().map_err(|_| {
linux_hello_common::Error::Config("Invalid boolean value".to_string())
})?;
}
"tpm.pcr_binding" => {
new_config.tpm.pcr_binding = value.parse()
.map_err(|_| linux_hello_common::Error::Config("Invalid boolean value".to_string()))?;
new_config.tpm.pcr_binding = value.parse().map_err(|_| {
linux_hello_common::Error::Config("Invalid boolean value".to_string())
})?;
}
_ => {
return Err(linux_hello_common::Error::Config(format!(
@@ -624,7 +667,7 @@ async fn cmd_config(config: &Config, json: bool, set: Option<&str>) -> Result<()
)));
}
}
// Save configuration
let config_path = "/etc/linux-hello/config.toml";
match new_config.save(config_path) {
@@ -634,10 +677,14 @@ async fn cmd_config(config: &Config, json: bool, set: Option<&str>) -> Result<()
let user_config_path = dirs_config_path();
std::fs::create_dir_all(user_config_path.parent().unwrap())?;
new_config.save(&user_config_path)?;
println!("Configuration saved to {} (no write access to {})", user_config_path.display(), config_path);
println!(
"Configuration saved to {} (no write access to {})",
user_config_path.display(),
config_path
);
}
}
return Ok(());
}