kroki_rs/diagrams/providers/
ditaa.rs

1use crate::diagrams::{DiagramError, DiagramProvider, DiagramResult};
2use async_trait::async_trait;
3use std::io::Write;
4use tempfile::NamedTempFile;
5use tokio::process::Command;
6
7crate::diagrams::define_provider!(DitaaProvider);
8
9#[async_trait]
10impl DiagramProvider for DitaaProvider {
11    fn validate(&self, source: &str) -> DiagramResult<()> {
12        if source.trim().is_empty() {
13            return Err(DiagramError::ValidationFailed(
14                "Diagram source is empty".into(),
15            ));
16        }
17        Ok(())
18    }
19
20    async fn generate(&self, source: &str, format: &str) -> DiagramResult<Vec<u8>> {
21        if format != "png" {
22            return Err(DiagramError::UnsupportedFormat {
23                format: format.into(),
24                provider: "Ditaa".into(),
25            });
26        }
27
28        let mut input_file = NamedTempFile::new().map_err(|e| {
29            DiagramError::Internal(format!("Failed to create temporary input file: {}", e))
30        })?;
31        input_file.write_all(source.as_bytes()).map_err(|e| {
32            DiagramError::Internal(format!("Failed to write source to temp file: {}", e))
33        })?;
34
35        let suffix = format!(".{}", format);
36        let mut output_file_builder = tempfile::Builder::new();
37        output_file_builder.suffix(&suffix);
38        let output_file_with_ext = output_file_builder.tempfile().map_err(|e| {
39            DiagramError::Internal(format!("Failed to create temp output file: {}", e))
40        })?;
41        let output_path = output_file_with_ext.path().to_path_buf();
42
43        // Detect if the binary is actually a JAR file (common in Debian/Ubuntu)
44        let is_jar = std::fs::File::open(&self.bin_path)
45            .map(|mut f| {
46                let mut buf = [0u8; 2];
47                use std::io::Read;
48                let _ = f.read_exact(&mut buf);
49                buf == *b"PK"
50            })
51            .unwrap_or(false);
52
53        let mut cmd = if is_jar {
54            let mut c = Command::new("java");
55            c.arg("-Djava.awt.headless=true")
56                .arg("-jar")
57                .arg(&self.bin_path);
58            c
59        } else {
60            Command::new(&self.bin_path)
61        };
62
63        cmd.arg(input_file.path());
64        cmd.arg(&output_path);
65        cmd.stdout(std::process::Stdio::piped())
66            .stderr(std::process::Stdio::piped());
67
68        let output = crate::diagrams::run_process_with_timeout(
69            "ditaa",
70            cmd,
71            None,
72            self.timeout_ms,
73            source.len(),
74        )
75        .await?;
76
77        if !output.status.success() {
78            let stderr = String::from_utf8_lossy(&output.stderr);
79            let stdout = String::from_utf8_lossy(&output.stdout);
80            return Err(DiagramError::ProcessFailed(format!(
81                "Ditaa conversion failed. stderr: {}, stdout: {}",
82                stderr, stdout
83            )));
84        }
85
86        let result = tokio::fs::read(&output_path).await.map_err(|e| {
87            DiagramError::Internal(format!("Failed to read ditaa output file: {}", e))
88        })?;
89
90        Ok(result)
91    }
92}