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::{
30 batch_format, batch_format_with_config, batch_lint, batch_lint_with_config, batch_validate,
31 batch_validate_with_config,
32};
33pub use completion::{generate_completion_for_command, print_installation_instructions};
34pub use convert::{
35 from_csv, from_json, from_parquet, from_toon, from_xml, from_yaml, to_csv, to_json, to_parquet,
36 to_toon, to_xml, to_yaml,
37};
38pub use format::format;
39pub use inspect::inspect;
40pub use lint::lint;
41pub use stats::stats;
42pub use validate::validate;
43
44use crate::error::CliError;
45use std::fs;
46use std::io::{self, Write};
47use std::path::PathBuf;
48
49/// Default maximum file size to prevent OOM attacks (1 GB)
50/// Can be overridden via `HEDL_MAX_FILE_SIZE` environment variable
51pub const DEFAULT_MAX_FILE_SIZE: u64 = 1024 * 1024 * 1024;
52
53/// Get the maximum file size from environment or use default.
54///
55/// Reads the `HEDL_MAX_FILE_SIZE` environment variable to allow customization
56/// of the maximum allowed file size. Falls back to [`DEFAULT_MAX_FILE_SIZE`] if
57/// the variable is not set or contains an invalid value.
58///
59/// # Examples
60///
61/// ```
62/// use hedl_cli::commands::DEFAULT_MAX_FILE_SIZE;
63///
64/// // Default behavior
65/// std::env::remove_var("HEDL_MAX_FILE_SIZE");
66/// // Note: get_max_file_size is private, so this example shows the concept
67/// // let size = get_max_file_size();
68/// // assert_eq!(size, DEFAULT_MAX_FILE_SIZE);
69///
70/// // Custom size via environment variable
71/// std::env::set_var("HEDL_MAX_FILE_SIZE", "500000000"); // 500 MB
72/// // let size = get_max_file_size();
73/// // assert_eq!(size, 500_000_000);
74/// ```
75fn get_max_file_size() -> u64 {
76 std::env::var("HEDL_MAX_FILE_SIZE")
77 .ok()
78 .and_then(|s| s.parse::<u64>().ok())
79 .unwrap_or(DEFAULT_MAX_FILE_SIZE)
80}
81
82/// Read a file from disk with size validation.
83///
84/// Reads the entire contents of a file into a string, with built-in protection
85/// against out-of-memory (OOM) attacks. Files larger than the configured maximum
86/// size will be rejected before reading.
87///
88/// # Arguments
89///
90/// * `path` - Path to the file to read
91///
92/// # Returns
93///
94/// Returns the file contents as a `String` on success.
95///
96/// # Errors
97///
98/// Returns `Err` if:
99/// - The file metadata cannot be accessed
100/// - The file size exceeds the maximum allowed size (configurable via `HEDL_MAX_FILE_SIZE`)
101/// - The file cannot be read
102/// - The file contains invalid UTF-8
103///
104/// # Examples
105///
106/// ```no_run
107/// use hedl_cli::commands::read_file;
108///
109/// # fn main() -> Result<(), hedl_cli::error::CliError> {
110/// // Read a small HEDL file
111/// let content = read_file("example.hedl")?;
112/// assert!(!content.is_empty());
113///
114/// // Files larger than the limit will fail
115/// std::env::set_var("HEDL_MAX_FILE_SIZE", "1000"); // 1 KB limit
116/// let result = read_file("large_file.hedl");
117/// assert!(result.is_err());
118/// # Ok(())
119/// # }
120/// ```
121///
122/// # Security
123///
124/// This function includes protection against OOM attacks by checking the file
125/// size before reading. The maximum file size defaults to 1 GB but can be
126/// customized via the `HEDL_MAX_FILE_SIZE` environment variable.
127///
128/// # Performance
129///
130/// Uses `fs::metadata()` to check file size before allocating memory, preventing
131/// unnecessary memory allocation for oversized files.
132pub fn read_file(path: &str) -> Result<String, CliError> {
133 // Check file size first to prevent reading extremely large files
134 let metadata = fs::metadata(path).map_err(|e| CliError::io_error(path, e))?;
135
136 let max_file_size = get_max_file_size();
137
138 if metadata.len() > max_file_size {
139 return Err(CliError::file_too_large(
140 path,
141 metadata.len(),
142 max_file_size,
143 ));
144 }
145
146 fs::read_to_string(path).map_err(|e| CliError::io_error(path, e))
147}
148
149/// Write content to a file or stdout.
150///
151/// Writes the provided content to either a specified file path or to stdout
152/// if no path is provided. This is the standard output mechanism used by
153/// all HEDL CLI commands.
154///
155/// # Arguments
156///
157/// * `content` - The string content to write
158/// * `path` - Optional output file path. If `None`, writes to stdout
159///
160/// # Returns
161///
162/// Returns `Ok(())` on success.
163///
164/// # Errors
165///
166/// Returns `Err` if:
167/// - File creation or writing fails (when `path` is `Some`)
168/// - Writing to stdout fails (when `path` is `None`)
169///
170/// # Examples
171///
172/// ```no_run
173/// use hedl_cli::commands::write_output;
174///
175/// # fn main() -> Result<(), hedl_cli::error::CliError> {
176/// // Write to stdout
177/// let hedl_content = "%VERSION: 1.0\n---\nteams: @Team[name]\n |t1,Team A\n |t2,Team B";
178/// write_output(hedl_content, None)?;
179///
180/// // Write to file
181/// write_output(hedl_content, Some("output.hedl"))?;
182/// # Ok(())
183/// # }
184/// ```
185///
186/// # Panics
187///
188/// Does not panic under normal circumstances. All I/O errors are returned
189/// as `Err` values.
190pub fn write_output(content: &str, path: Option<&str>) -> Result<(), CliError> {
191 match path {
192 Some(p) => fs::write(p, content).map_err(|e| CliError::io_error(p, e)),
193 None => io::stdout()
194 .write_all(content.as_bytes())
195 .map_err(|e| CliError::Io {
196 path: PathBuf::from("<stdout>"),
197 message: e.to_string(),
198 }),
199 }
200}