hedl_cli/commands/
mod.rs

1// Dweve HEDL - Hierarchical Entity Data Language
2//
3// Copyright (c) 2025 Dweve IP B.V. and individual contributors.
4//
5// SPDX-License-Identifier: Apache-2.0
6//
7// Licensed under the Apache License, Version 2.0 (the "License");
8// you may not use this file except in compliance with the License.
9// You may obtain a copy of the License in the LICENSE file at the
10// root of this repository or at: http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! CLI command implementations
19
20mod batch_commands;
21mod completion;
22mod convert;
23mod format;
24mod inspect;
25mod lint;
26mod stats;
27mod validate;
28
29pub use batch_commands::{batch_format, batch_lint, batch_validate};
30pub use completion::{generate_completion_for_command, print_installation_instructions};
31pub use convert::{
32    from_csv, from_json, from_parquet, from_xml, from_yaml, to_csv, to_json, to_parquet, to_toon,
33    to_xml, to_yaml,
34};
35pub use format::format;
36pub use inspect::inspect;
37pub use lint::lint;
38pub use stats::stats;
39pub use validate::validate;
40
41use std::fs;
42use std::io::{self, Write};
43
44/// Default maximum file size to prevent OOM attacks (1 GB)
45/// Can be overridden via HEDL_MAX_FILE_SIZE environment variable
46pub const DEFAULT_MAX_FILE_SIZE: u64 = 1024 * 1024 * 1024;
47
48/// Get the maximum file size from environment or use default.
49///
50/// Reads the `HEDL_MAX_FILE_SIZE` environment variable to allow customization
51/// of the maximum allowed file size. Falls back to [`DEFAULT_MAX_FILE_SIZE`] if
52/// the variable is not set or contains an invalid value.
53///
54/// # Examples
55///
56/// ```
57/// use hedl_cli::commands::DEFAULT_MAX_FILE_SIZE;
58///
59/// // Default behavior
60/// std::env::remove_var("HEDL_MAX_FILE_SIZE");
61/// // Note: get_max_file_size is private, so this example shows the concept
62/// // let size = get_max_file_size();
63/// // assert_eq!(size, DEFAULT_MAX_FILE_SIZE);
64///
65/// // Custom size via environment variable
66/// std::env::set_var("HEDL_MAX_FILE_SIZE", "500000000"); // 500 MB
67/// // let size = get_max_file_size();
68/// // assert_eq!(size, 500_000_000);
69/// ```
70fn get_max_file_size() -> u64 {
71    std::env::var("HEDL_MAX_FILE_SIZE")
72        .ok()
73        .and_then(|s| s.parse::<u64>().ok())
74        .unwrap_or(DEFAULT_MAX_FILE_SIZE)
75}
76
77/// Read a file from disk with size validation.
78///
79/// Reads the entire contents of a file into a string, with built-in protection
80/// against out-of-memory (OOM) attacks. Files larger than the configured maximum
81/// size will be rejected before reading.
82///
83/// # Arguments
84///
85/// * `path` - Path to the file to read
86///
87/// # Returns
88///
89/// Returns the file contents as a `String` on success.
90///
91/// # Errors
92///
93/// Returns `Err` if:
94/// - The file metadata cannot be accessed
95/// - The file size exceeds the maximum allowed size (configurable via `HEDL_MAX_FILE_SIZE`)
96/// - The file cannot be read
97/// - The file contains invalid UTF-8
98///
99/// # Examples
100///
101/// ```no_run
102/// use hedl_cli::commands::read_file;
103///
104/// # fn main() -> Result<(), String> {
105/// // Read a small HEDL file
106/// let content = read_file("example.hedl")?;
107/// assert!(!content.is_empty());
108///
109/// // Files larger than the limit will fail
110/// std::env::set_var("HEDL_MAX_FILE_SIZE", "1000"); // 1 KB limit
111/// let result = read_file("large_file.hedl");
112/// assert!(result.is_err());
113/// # Ok(())
114/// # }
115/// ```
116///
117/// # Security
118///
119/// This function includes protection against OOM attacks by checking the file
120/// size before reading. The maximum file size defaults to 1 GB but can be
121/// customized via the `HEDL_MAX_FILE_SIZE` environment variable.
122///
123/// # Performance
124///
125/// Uses `fs::metadata()` to check file size before allocating memory, preventing
126/// unnecessary memory allocation for oversized files.
127pub fn read_file(path: &str) -> Result<String, String> {
128    // Check file size first to prevent reading extremely large files
129    let metadata = fs::metadata(path)
130        .map_err(|e| format!("Failed to get metadata for '{}': {}", path, e))?;
131
132    let max_file_size = get_max_file_size();
133
134    if metadata.len() > max_file_size {
135        return Err(format!(
136            "File '{}' is too large ({} bytes). Maximum allowed size is {} bytes ({} MB).\n\
137             To process larger files, set HEDL_MAX_FILE_SIZE environment variable (in bytes).",
138            path,
139            metadata.len(),
140            max_file_size,
141            max_file_size / (1024 * 1024)
142        ));
143    }
144
145    fs::read_to_string(path).map_err(|e| format!("Failed to read '{}': {}", path, e))
146}
147
148/// Write content to a file or stdout.
149///
150/// Writes the provided content to either a specified file path or to stdout
151/// if no path is provided. This is the standard output mechanism used by
152/// all HEDL CLI commands.
153///
154/// # Arguments
155///
156/// * `content` - The string content to write
157/// * `path` - Optional output file path. If `None`, writes to stdout
158///
159/// # Returns
160///
161/// Returns `Ok(())` on success.
162///
163/// # Errors
164///
165/// Returns `Err` if:
166/// - File creation or writing fails (when `path` is `Some`)
167/// - Writing to stdout fails (when `path` is `None`)
168///
169/// # Examples
170///
171/// ```no_run
172/// use hedl_cli::commands::write_output;
173///
174/// # fn main() -> Result<(), String> {
175/// // Write to stdout
176/// let hedl_content = "%VERSION: 1.0\n---\nteams: @Team[name]\n  |t1,Team A\n  |t2,Team B";
177/// write_output(hedl_content, None)?;
178///
179/// // Write to file
180/// write_output(hedl_content, Some("output.hedl"))?;
181/// # Ok(())
182/// # }
183/// ```
184///
185/// # Panics
186///
187/// Does not panic under normal circumstances. All I/O errors are returned
188/// as `Err` values.
189pub fn write_output(content: &str, path: Option<&str>) -> Result<(), String> {
190    match path {
191        Some(p) => fs::write(p, content).map_err(|e| format!("Failed to write '{}': {}", p, e)),
192        None => io::stdout()
193            .write_all(content.as_bytes())
194            .map_err(|e| format!("Failed to write to stdout: {}", e)),
195    }
196}