use crate::ports::outbound::OutputPresenter;
use crate::shared::error::SbomError;
use crate::shared::security::validate_not_symlink;
use crate::shared::Result;
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
pub struct FileSystemWriter {
output_path: PathBuf,
}
impl FileSystemWriter {
pub fn new(output_path: PathBuf) -> Self {
Self { output_path }
}
fn validate_parent_directory(&self) -> Result<()> {
if let Some(parent) = self.output_path.parent() {
if !parent.exists() && parent != Path::new("") {
return Err(SbomError::FileWriteError {
path: self.output_path.clone(),
details: format!("Parent directory does not exist: {}", parent.display()),
}
.into());
}
}
Ok(())
}
fn validate_output_security(&self) -> Result<()> {
if self.output_path.exists() {
validate_not_symlink(&self.output_path, "write").map_err(|e| {
SbomError::FileWriteError {
path: self.output_path.clone(),
details: e.to_string(),
}
})?;
}
if let Some(parent) = self.output_path.parent() {
if parent.exists() {
match parent.canonicalize() {
Ok(_canonical) => {
if let Ok(_original) = parent.canonicalize() {
}
}
Err(e) => {
return Err(SbomError::FileWriteError {
path: self.output_path.clone(),
details: format!("Failed to validate parent directory: {}", e),
}
.into());
}
}
}
}
Ok(())
}
}
impl OutputPresenter for FileSystemWriter {
fn present(&self, content: &str) -> Result<()> {
self.validate_parent_directory()?;
self.validate_output_security()?;
fs::write(&self.output_path, content).map_err(|e| SbomError::FileWriteError {
path: self.output_path.clone(),
details: e.to_string(),
})?;
eprintln!("✅ Output complete: {}", self.output_path.display());
Ok(())
}
}
pub struct StdoutPresenter;
impl StdoutPresenter {
pub fn new() -> Self {
Self
}
}
impl Default for StdoutPresenter {
fn default() -> Self {
Self::new()
}
}
impl OutputPresenter for StdoutPresenter {
fn present(&self, content: &str) -> Result<()> {
io::stdout()
.write_all(content.as_bytes())
.map_err(|e| anyhow::anyhow!("Failed to write to stdout: {}", e))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_file_writer_success() {
let temp_dir = TempDir::new().unwrap();
let output_path = temp_dir.path().join("output.json");
let writer = FileSystemWriter::new(output_path.clone());
let result = writer.present("test content");
assert!(result.is_ok());
let written_content = fs::read_to_string(&output_path).unwrap();
assert_eq!(written_content, "test content");
}
#[test]
fn test_file_writer_parent_directory_not_found() {
let output_path = PathBuf::from("/nonexistent/directory/output.json");
let writer = FileSystemWriter::new(output_path);
let result = writer.present("test content");
assert!(result.is_err());
let err_string = format!("{}", result.unwrap_err());
assert!(err_string.contains("Parent directory does not exist"));
}
#[test]
fn test_stdout_presenter_success() {
let presenter = StdoutPresenter::new();
let result = presenter.present("test output\n");
assert!(result.is_ok());
}
}