1use std::{
4 io,
5 path::{Path, PathBuf},
6 sync::Arc,
7};
8
9use hcl_edit::parser::parse_body;
10use tracing::instrument;
11
12use super::{LoaderLimits, RawBlock, RawComponent, SourceMap, lowering, source_map::LineIndex};
13use crate::{
14 Diagnostic, LimitKind, Result, Severity,
15 diagnostic::Diagnostic as Diag,
16 discovery::{DirKind, DiscoveredDir},
17 ir::{ComponentKind, FileExt},
18 util::paths,
19};
20
21#[derive(Debug)]
26#[non_exhaustive]
27pub struct LoadContext<'a> {
28 pub root: &'a Path,
30 pub sources: &'a SourceMap,
33 pub limits: &'a LoaderLimits,
35}
36
37impl<'a> LoadContext<'a> {
38 #[must_use]
41 pub const fn new(root: &'a Path, sources: &'a SourceMap, limits: &'a LoaderLimits) -> Self {
42 Self {
43 root,
44 sources,
45 limits,
46 }
47 }
48}
49
50pub trait Loader: Send + Sync {
54 fn load(&self, dir: &DiscoveredDir, ctx: &LoadContext<'_>) -> Result<RawComponent>;
63}
64
65#[derive(Clone, Copy, Debug, Default)]
71pub struct HclEditLoader;
72
73impl HclEditLoader {
74 #[must_use]
76 pub const fn new() -> Self {
77 Self
78 }
79
80 #[must_use]
84 pub fn parse_bytes(
85 &self,
86 bytes: &[u8],
87 path: &Arc<Path>,
88 limits: &LoaderLimits,
89 ) -> ParseBytesResult {
90 let mut diagnostics: Vec<Diagnostic> = Vec::new();
91
92 if bytes.len() > limits.max_file_bytes as usize {
93 diagnostics.push(Diag::limit(
94 LimitKind::FileSize,
95 "TF1201",
96 format!(
97 "file exceeds loader byte cap ({} > {}); skipped",
98 bytes.len(),
99 limits.max_file_bytes
100 ),
101 ));
102 return ParseBytesResult {
103 blocks: Vec::new(),
104 diagnostics,
105 };
106 }
107
108 let src = match std::str::from_utf8(bytes) {
109 Ok(s) => s,
110 Err(err) => {
111 diagnostics.push(Diag::new(
112 Severity::Warn,
113 "TF1202",
114 format!("file is not valid UTF-8: {err}"),
115 ));
116 return ParseBytesResult {
117 blocks: Vec::new(),
118 diagnostics,
119 };
120 }
121 };
122
123 let body = match parse_body(src) {
124 Ok(b) => b,
125 Err(err) => {
126 diagnostics.push(Diag::new(
127 Severity::Warn,
128 "TF1203",
129 format!("HCL parse error: {err}"),
130 ));
131 return ParseBytesResult {
132 blocks: Vec::new(),
133 diagnostics,
134 };
135 }
136 };
137
138 let line_index = LineIndex::build(src);
139 let lowered = lowering::lower_body(&body, path, &line_index, limits, src.len());
140 diagnostics.extend(lowered.diagnostics);
141 ParseBytesResult {
142 blocks: lowered.blocks,
143 diagnostics,
144 }
145 }
146}
147
148#[derive(Debug, Default)]
151pub struct ParseBytesResult {
152 pub blocks: Vec<RawBlock>,
154 pub diagnostics: Vec<Diagnostic>,
156}
157
158impl Loader for HclEditLoader {
159 #[instrument(level = "debug", skip(self, ctx), fields(dir = %dir.path.display()))]
160 fn load(&self, dir: &DiscoveredDir, ctx: &LoadContext<'_>) -> Result<RawComponent> {
161 let kind = match dir.kind {
162 DirKind::Component => ComponentKind::Component,
163 DirKind::Module => ComponentKind::Module,
164 DirKind::Environments | DirKind::Files | DirKind::Other => {
165 ComponentKind::Module
169 }
170 };
171 let mut raw = RawComponent::new(Arc::clone(&dir.path), kind);
172
173 for file in &dir.files {
174 if !file.ext.is_hcl() {
175 continue;
176 }
177 let abs = match resolve_under_root(&file.path, ctx.root) {
181 Ok(p) => p,
182 Err(diag) => {
183 raw.diagnostics.push(diag);
184 continue;
185 }
186 };
187
188 let bytes = match read_file(&abs, ctx.limits.max_file_bytes) {
189 Ok(b) => b,
190 Err(LoaderReadError::TooLarge { observed, limit }) => {
191 raw.diagnostics.push(Diag::limit(
192 LimitKind::FileSize,
193 "TF1201",
194 format!(
195 "file exceeds loader byte cap ({observed} > {limit}); skipped: {}",
196 file.path.display()
197 ),
198 ));
199 continue;
200 }
201 Err(LoaderReadError::Io { source }) => {
202 raw.diagnostics.push(Diag::new(
203 Severity::Warn,
204 "TF1204",
205 format!("i/o error reading {}: {source}", file.path.display()),
206 ));
207 continue;
208 }
209 };
210
211 let src = match std::str::from_utf8(&bytes) {
212 Ok(s) => Arc::<str>::from(s),
213 Err(err) => {
214 raw.diagnostics.push(Diag::new(
215 Severity::Warn,
216 "TF1202",
217 format!("file is not valid UTF-8: {} ({err})", file.path.display()),
218 ));
219 continue;
220 }
221 };
222
223 ctx.sources.insert(&abs, Arc::clone(&src));
224
225 let body = match parse_body(&src) {
226 Ok(b) => b,
227 Err(err) => {
228 raw.diagnostics.push(Diag::new(
229 Severity::Warn,
230 "TF1203",
231 format!("HCL parse error in {}: {err}", file.path.display()),
232 ));
233 continue;
234 }
235 };
236
237 let line_index = LineIndex::build(&src);
238 let path_arc: Arc<Path> = Arc::clone(&file.path);
239 let lowered =
240 lowering::lower_body(&body, &path_arc, &line_index, ctx.limits, src.len());
241 raw.diagnostics.extend(lowered.diagnostics);
242 for block in lowered.blocks {
243 if !file_ext_supports_block_kind(file.ext, block.kind) {
244 raw.diagnostics.push(Diag::new(
245 Severity::Trace,
246 "TF1205",
247 format!(
248 "block `{:?}` in unexpected file extension `{:?}`: {}",
249 block.kind,
250 file.ext,
251 file.path.display()
252 ),
253 ));
254 }
255 raw.raw_blocks.push(block);
256 }
257 }
258
259 Ok(raw)
260 }
261}
262
263enum LoaderReadError {
264 TooLarge { observed: u64, limit: u32 },
265 Io { source: io::Error },
266}
267
268fn read_file(path: &Path, max_bytes: u32) -> std::result::Result<Vec<u8>, LoaderReadError> {
269 let metadata = std::fs::metadata(path).map_err(|source| LoaderReadError::Io { source })?;
270 let len = metadata.len();
271 if len > u64::from(max_bytes) {
272 return Err(LoaderReadError::TooLarge {
273 observed: len,
274 limit: max_bytes,
275 });
276 }
277 std::fs::read(path).map_err(|source| LoaderReadError::Io { source })
278}
279
280fn resolve_under_root(rel: &Arc<Path>, root: &Path) -> std::result::Result<PathBuf, Diagnostic> {
281 let candidate = root.join(rel);
282 paths::canonicalize_inside(&candidate, root, paths::SymlinkPolicy::Reject)
283 .map_err(|err| Diag::new(Severity::Warn, "TF1206", format!("path safety: {err}")))
284}
285
286const fn file_ext_supports_block_kind(ext: FileExt, kind: BlockKind) -> bool {
287 use BlockKind as B;
288 match (ext, kind) {
289 (FileExt::Tfvars, B::Unknown) => true,
290 (FileExt::Tf, B::Include | B::Generate | B::Dependency | B::Inputs)
291 | (FileExt::Tfvars | FileExt::Json, _) => false,
292 (FileExt::Tf | FileExt::Hcl | FileExt::TerragruntHcl, _) => true,
293 }
294}
295
296use crate::ir::BlockKind;
298
299#[cfg(test)]
301const _ASSERT_LOADER_IS_OBJECT_SAFE: Option<&dyn Loader> = None;