1pub mod cache;
39pub mod config;
40pub mod environment;
41pub mod hooks;
42pub mod manifest;
43pub mod secrets;
44pub mod shell;
45pub mod tasks;
46
47pub const VERSION: &str = env!("CARGO_PKG_VERSION");
49
50#[cfg(test)]
51mod schema_tests;
52
53use miette::{Diagnostic, SourceSpan};
54use serde::{Deserialize, Serialize};
55use std::path::{Path, PathBuf};
56use thiserror::Error;
57
58#[derive(Error, Debug, Diagnostic)]
60pub enum Error {
61 #[error("Configuration error: {message}")]
62 #[diagnostic(
63 code(cuenv::config::invalid),
64 help("Check your cuenv.cue configuration file for syntax errors or invalid values")
65 )]
66 Configuration {
67 #[source_code]
68 src: String,
69 #[label("invalid configuration")]
70 span: Option<SourceSpan>,
71 message: String,
72 },
73
74 #[error("FFI operation failed in {function}: {message}")]
75 #[diagnostic(code(cuenv::ffi::error))]
76 Ffi {
77 function: &'static str,
78 message: String,
79 #[help]
80 help: Option<String>,
81 },
82
83 #[error("CUE parsing failed: {message}")]
84 #[diagnostic(code(cuenv::cue::parse_error))]
85 CueParse {
86 path: Box<Path>,
87 #[source_code]
88 src: Option<String>,
89 #[label("parsing failed here")]
90 span: Option<SourceSpan>,
91 message: String,
92 suggestions: Option<Vec<String>>,
93 },
94
95 #[error("I/O operation failed")]
96 #[diagnostic(
97 code(cuenv::io::error),
98 help("Check file permissions and ensure the path exists")
99 )]
100 Io {
101 #[source]
102 source: std::io::Error,
103 path: Option<Box<Path>>,
104 operation: String,
105 },
106
107 #[error("Text encoding error")]
108 #[diagnostic(
109 code(cuenv::encoding::utf8),
110 help("The file contains invalid UTF-8. Ensure your files use UTF-8 encoding.")
111 )]
112 Utf8 {
113 #[source]
114 source: std::str::Utf8Error,
115 file: Option<Box<Path>>,
116 },
117
118 #[error("Operation timed out after {seconds} seconds")]
119 #[diagnostic(
120 code(cuenv::timeout),
121 help("Try increasing the timeout or check if the operation is stuck")
122 )]
123 Timeout { seconds: u64 },
124
125 #[error("Validation failed: {message}")]
126 #[diagnostic(code(cuenv::validation::failed))]
127 Validation {
128 #[source_code]
129 src: Option<String>,
130 #[label("validation failed")]
131 span: Option<SourceSpan>,
132 message: String,
133 #[related]
134 related: Vec<Error>,
135 },
136}
137
138impl Error {
139 pub fn configuration(msg: impl Into<String>) -> Self {
140 Error::Configuration {
141 src: String::new(),
142 span: None,
143 message: msg.into(),
144 }
145 }
146
147 pub fn configuration_with_source(
148 msg: impl Into<String>,
149 src: impl Into<String>,
150 span: Option<SourceSpan>,
151 ) -> Self {
152 Error::Configuration {
153 src: src.into(),
154 span,
155 message: msg.into(),
156 }
157 }
158
159 pub fn ffi(function: &'static str, message: impl Into<String>) -> Self {
160 Error::Ffi {
161 function,
162 message: message.into(),
163 help: None,
164 }
165 }
166
167 pub fn ffi_with_help(
168 function: &'static str,
169 message: impl Into<String>,
170 help: impl Into<String>,
171 ) -> Self {
172 Error::Ffi {
173 function,
174 message: message.into(),
175 help: Some(help.into()),
176 }
177 }
178
179 pub fn cue_parse(path: &Path, message: impl Into<String>) -> Self {
180 Error::CueParse {
181 path: path.into(),
182 src: None,
183 span: None,
184 message: message.into(),
185 suggestions: None,
186 }
187 }
188
189 pub fn cue_parse_with_source(
190 path: &Path,
191 message: impl Into<String>,
192 src: impl Into<String>,
193 span: Option<SourceSpan>,
194 suggestions: Option<Vec<String>>,
195 ) -> Self {
196 Error::CueParse {
197 path: path.into(),
198 src: Some(src.into()),
199 span,
200 message: message.into(),
201 suggestions,
202 }
203 }
204
205 pub fn validation(msg: impl Into<String>) -> Self {
206 Error::Validation {
207 src: None,
208 span: None,
209 message: msg.into(),
210 related: Vec::new(),
211 }
212 }
213
214 pub fn validation_with_source(
215 msg: impl Into<String>,
216 src: impl Into<String>,
217 span: Option<SourceSpan>,
218 ) -> Self {
219 Error::Validation {
220 src: Some(src.into()),
221 span,
222 message: msg.into(),
223 related: Vec::new(),
224 }
225 }
226}
227
228impl From<std::io::Error> for Error {
230 fn from(source: std::io::Error) -> Self {
231 Error::Io {
232 source,
233 path: None,
234 operation: "unknown".to_string(),
235 }
236 }
237}
238
239impl From<std::str::Utf8Error> for Error {
240 fn from(source: std::str::Utf8Error) -> Self {
241 Error::Utf8 { source, file: None }
242 }
243}
244
245pub type Result<T> = std::result::Result<T, Error>;
247
248pub struct Limits {
250 pub max_path_length: usize,
251 pub max_package_name_length: usize,
252 pub max_output_size: usize,
253}
254
255impl Default for Limits {
256 fn default() -> Self {
257 Self {
258 max_path_length: 4096,
259 max_package_name_length: 256,
260 max_output_size: 100 * 1024 * 1024, }
262 }
263}
264
265#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
285pub struct PackageDir(PathBuf);
286
287impl PackageDir {
288 #[must_use]
290 pub fn as_path(&self) -> &Path {
291 &self.0
292 }
293
294 #[must_use]
296 pub fn into_path_buf(self) -> PathBuf {
297 self.0
298 }
299}
300
301impl AsRef<Path> for PackageDir {
302 fn as_ref(&self) -> &Path {
303 &self.0
304 }
305}
306
307#[derive(Error, Debug, Clone, Diagnostic)]
309pub enum PackageDirError {
310 #[error("path does not exist: {0}")]
312 #[diagnostic(
313 code(cuenv::package_dir::not_found),
314 help("Make sure the directory exists and you have permission to access it")
315 )]
316 NotFound(String),
317
318 #[error("path is not a directory: {0}")]
320 #[diagnostic(
321 code(cuenv::package_dir::not_directory),
322 help("The path must point to a directory, not a file")
323 )]
324 NotADirectory(String),
325
326 #[error("io error accessing path: {0}")]
328 #[diagnostic(
329 code(cuenv::package_dir::io_error),
330 help("Check file permissions and ensure you have access to the path")
331 )]
332 Io(String),
333}
334
335impl TryFrom<&Path> for PackageDir {
336 type Error = PackageDirError;
337
338 fn try_from(input: &Path) -> std::result::Result<Self, Self::Error> {
352 match std::fs::metadata(input) {
353 Ok(meta) => {
354 if meta.is_dir() {
355 Ok(PackageDir(input.to_path_buf()))
356 } else {
357 Err(PackageDirError::NotADirectory(input.display().to_string()))
358 }
359 }
360 Err(e) => {
361 if e.kind() == std::io::ErrorKind::NotFound {
362 Err(PackageDirError::NotFound(input.display().to_string()))
363 } else {
364 Err(PackageDirError::Io(e.to_string()))
365 }
366 }
367 }
368 }
369}
370
371#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
394pub struct PackageName(String);
395
396impl PackageName {
397 #[must_use]
399 pub fn as_str(&self) -> &str {
400 &self.0
401 }
402
403 #[must_use]
405 pub fn into_string(self) -> String {
406 self.0
407 }
408}
409
410impl AsRef<str> for PackageName {
411 fn as_ref(&self) -> &str {
412 &self.0
413 }
414}
415
416impl std::fmt::Display for PackageName {
417 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
418 write!(f, "{}", self.0)
419 }
420}
421
422#[derive(Error, Debug, Clone, Diagnostic)]
424pub enum PackageNameError {
425 #[error("invalid package name: {0}")]
427 #[diagnostic(
428 code(cuenv::package_name::invalid),
429 help(
430 "Package names must be 1-64 characters, start with alphanumeric, and contain only alphanumeric, hyphen, or underscore characters"
431 )
432 )]
433 Invalid(String),
434}
435
436impl TryFrom<&str> for PackageName {
437 type Error = PackageNameError;
438
439 fn try_from(s: &str) -> std::result::Result<Self, Self::Error> {
452 let bytes = s.as_bytes();
453
454 if bytes.is_empty() || bytes.len() > 64 {
456 return Err(PackageNameError::Invalid(s.to_string()));
457 }
458
459 let first = bytes[0];
461 let is_alnum =
462 |b: u8| b.is_ascii_uppercase() || b.is_ascii_lowercase() || b.is_ascii_digit();
463
464 if !is_alnum(first) {
465 return Err(PackageNameError::Invalid(s.to_string()));
466 }
467
468 let valid = |b: u8| is_alnum(b) || b == b'-' || b == b'_';
470 for &b in bytes {
471 if !valid(b) {
472 return Err(PackageNameError::Invalid(s.to_string()));
473 }
474 }
475
476 Ok(PackageName(s.to_string()))
477 }
478}
479
480impl TryFrom<String> for PackageName {
481 type Error = PackageNameError;
482
483 fn try_from(s: String) -> std::result::Result<Self, Self::Error> {
485 Self::try_from(s.as_str())
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492 use miette::SourceSpan;
493 use std::path::Path;
494
495 #[test]
496 fn test_error_configuration() {
497 let err = Error::configuration("test message");
498 assert_eq!(err.to_string(), "Configuration error: test message");
499
500 if let Error::Configuration { message, .. } = err {
501 assert_eq!(message, "test message");
502 } else {
503 panic!("Expected Configuration error");
504 }
505 }
506
507 #[test]
508 fn test_error_configuration_with_source() {
509 let src = "test source code";
510 let span = SourceSpan::from(0..4);
511 let err = Error::configuration_with_source("config error", src, Some(span));
512
513 if let Error::Configuration {
514 src: source,
515 span: s,
516 message,
517 } = err
518 {
519 assert_eq!(source, "test source code");
520 assert_eq!(s, Some(SourceSpan::from(0..4)));
521 assert_eq!(message, "config error");
522 } else {
523 panic!("Expected Configuration error");
524 }
525 }
526
527 #[test]
528 fn test_error_ffi() {
529 let err = Error::ffi("test_function", "FFI failed");
530 assert_eq!(
531 err.to_string(),
532 "FFI operation failed in test_function: FFI failed"
533 );
534
535 if let Error::Ffi {
536 function,
537 message,
538 help,
539 } = err
540 {
541 assert_eq!(function, "test_function");
542 assert_eq!(message, "FFI failed");
543 assert!(help.is_none());
544 } else {
545 panic!("Expected Ffi error");
546 }
547 }
548
549 #[test]
550 fn test_error_ffi_with_help() {
551 let err = Error::ffi_with_help("test_func", "error msg", "try this instead");
552
553 if let Error::Ffi {
554 function,
555 message,
556 help,
557 } = err
558 {
559 assert_eq!(function, "test_func");
560 assert_eq!(message, "error msg");
561 assert_eq!(help, Some("try this instead".to_string()));
562 } else {
563 panic!("Expected Ffi error");
564 }
565 }
566
567 #[test]
568 fn test_error_cue_parse() {
569 let path = Path::new("/test/path.cue");
570 let err = Error::cue_parse(path, "parsing failed");
571 assert_eq!(err.to_string(), "CUE parsing failed: parsing failed");
572
573 if let Error::CueParse {
574 path: p, message, ..
575 } = err
576 {
577 assert_eq!(p.as_ref(), Path::new("/test/path.cue"));
578 assert_eq!(message, "parsing failed");
579 } else {
580 panic!("Expected CueParse error");
581 }
582 }
583
584 #[test]
585 fn test_error_cue_parse_with_source() {
586 let path = Path::new("/test/file.cue");
587 let src = "package test";
588 let span = SourceSpan::from(0..7);
589 let suggestions = vec!["Check syntax".to_string(), "Verify imports".to_string()];
590
591 let err = Error::cue_parse_with_source(
592 path,
593 "parse error",
594 src,
595 Some(span),
596 Some(suggestions.clone()),
597 );
598
599 if let Error::CueParse {
600 path: p,
601 src: source,
602 span: s,
603 message,
604 suggestions: sugg,
605 } = err
606 {
607 assert_eq!(p.as_ref(), Path::new("/test/file.cue"));
608 assert_eq!(source, Some("package test".to_string()));
609 assert_eq!(s, Some(SourceSpan::from(0..7)));
610 assert_eq!(message, "parse error");
611 assert_eq!(sugg, Some(suggestions));
612 } else {
613 panic!("Expected CueParse error");
614 }
615 }
616
617 #[test]
618 fn test_error_validation() {
619 let err = Error::validation("validation failed");
620 assert_eq!(err.to_string(), "Validation failed: validation failed");
621
622 if let Error::Validation {
623 message, related, ..
624 } = err
625 {
626 assert_eq!(message, "validation failed");
627 assert!(related.is_empty());
628 } else {
629 panic!("Expected Validation error");
630 }
631 }
632
633 #[test]
634 fn test_error_validation_with_source() {
635 let src = "test validation source";
636 let span = SourceSpan::from(5..15);
637 let err = Error::validation_with_source("validation error", src, Some(span));
638
639 if let Error::Validation {
640 src: source,
641 span: s,
642 message,
643 ..
644 } = err
645 {
646 assert_eq!(source, Some("test validation source".to_string()));
647 assert_eq!(s, Some(SourceSpan::from(5..15)));
648 assert_eq!(message, "validation error");
649 } else {
650 panic!("Expected Validation error");
651 }
652 }
653
654 #[test]
655 fn test_error_timeout() {
656 let err = Error::Timeout { seconds: 30 };
657 assert_eq!(err.to_string(), "Operation timed out after 30 seconds");
658 }
659
660 #[test]
661 fn test_error_from_io_error() {
662 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
663 let err: Error = io_err.into();
664
665 if let Error::Io { operation, .. } = err {
666 assert_eq!(operation, "unknown");
667 } else {
668 panic!("Expected Io error");
669 }
670 }
671
672 #[test]
673 fn test_error_from_utf8_error() {
674 let bytes = vec![0xFF, 0xFE];
675 let utf8_err = std::str::from_utf8(&bytes).unwrap_err();
676 let err: Error = utf8_err.into();
677
678 assert!(matches!(err, Error::Utf8 { .. }));
679 }
680
681 #[test]
682 fn test_limits_default() {
683 let limits = Limits::default();
684 assert_eq!(limits.max_path_length, 4096);
685 assert_eq!(limits.max_package_name_length, 256);
686 assert_eq!(limits.max_output_size, 100 * 1024 * 1024);
687 }
688
689 #[test]
690 fn test_result_type_alias() {
691 let ok_result: Result<i32> = Ok(42);
692 assert!(ok_result.is_ok());
693 if let Ok(value) = ok_result {
694 assert_eq!(value, 42);
695 }
696
697 let err_result: Result<i32> = Err(Error::configuration("test"));
698 assert!(err_result.is_err());
699 }
700
701 #[test]
702 fn test_error_display() {
703 let errors = vec![
704 (Error::configuration("test"), "Configuration error: test"),
705 (
706 Error::ffi("func", "msg"),
707 "FFI operation failed in func: msg",
708 ),
709 (
710 Error::cue_parse(Path::new("/test"), "msg"),
711 "CUE parsing failed: msg",
712 ),
713 (Error::validation("msg"), "Validation failed: msg"),
714 (
715 Error::Timeout { seconds: 10 },
716 "Operation timed out after 10 seconds",
717 ),
718 ];
719
720 for (error, expected) in errors {
721 assert_eq!(error.to_string(), expected);
722 }
723 }
724
725 #[test]
726 fn test_error_diagnostic_codes() {
727 use miette::Diagnostic;
728
729 let config_err = Error::configuration("test");
730 assert_eq!(
731 config_err.code().unwrap().to_string(),
732 "cuenv::config::invalid"
733 );
734
735 let ffi_err = Error::ffi("func", "msg");
736 assert_eq!(ffi_err.code().unwrap().to_string(), "cuenv::ffi::error");
737
738 let cue_err = Error::cue_parse(Path::new("/test"), "msg");
739 assert_eq!(
740 cue_err.code().unwrap().to_string(),
741 "cuenv::cue::parse_error"
742 );
743
744 let validation_err = Error::validation("msg");
745 assert_eq!(
746 validation_err.code().unwrap().to_string(),
747 "cuenv::validation::failed"
748 );
749
750 let timeout_err = Error::Timeout { seconds: 5 };
751 assert_eq!(timeout_err.code().unwrap().to_string(), "cuenv::timeout");
752 }
753
754 #[test]
755 fn test_package_dir_validation() {
756 let result = PackageDir::try_from(Path::new("."));
758 assert!(result.is_ok(), "Current directory should be valid");
759
760 let pkg_dir = result.unwrap();
762 assert_eq!(pkg_dir.as_path(), Path::new("."));
763 assert_eq!(pkg_dir.as_ref(), Path::new("."));
764 assert_eq!(pkg_dir.into_path_buf(), PathBuf::from("."));
765
766 let result = PackageDir::try_from(Path::new("/path/does/not/exist"));
768 assert!(result.is_err());
769 match result.unwrap_err() {
770 PackageDirError::NotFound(_) => {} other => panic!("Expected NotFound error, got: {:?}", other),
772 }
773
774 let temp_path = std::env::temp_dir().join("cuenv_test_file");
777 let file = std::fs::File::create(&temp_path).unwrap();
778 drop(file);
779
780 let result = PackageDir::try_from(temp_path.as_path());
781 assert!(result.is_err());
782 match result.unwrap_err() {
783 PackageDirError::NotADirectory(_) => {} other => panic!("Expected NotADirectory error, got: {:?}", other),
785 }
786
787 std::fs::remove_file(temp_path).ok();
789 }
790
791 #[test]
792 fn test_package_name_validation() {
793 let max_len_string = "a".repeat(64);
795 let valid_names = vec![
796 "my-package",
797 "package_123",
798 "a", "A", "0package", "package-with-hyphens",
802 "package_with_underscores",
803 max_len_string.as_str(), ];
805
806 for name in valid_names {
807 let result = PackageName::try_from(name);
808 assert!(result.is_ok(), "'{}' should be valid", name);
809
810 let result = PackageName::try_from(name.to_string());
812 assert!(result.is_ok(), "'{}' as String should be valid", name);
813
814 let pkg_name = result.unwrap();
816 assert_eq!(pkg_name.as_str(), name);
817 assert_eq!(pkg_name.as_ref(), name);
818 assert_eq!(pkg_name.to_string(), name);
819 assert_eq!(pkg_name.into_string(), name.to_string());
820 }
821
822 let too_long_string = "a".repeat(65);
824 let invalid_names = vec![
825 "", "-invalid", "_invalid", "invalid.name", "invalid/name", "invalid:name", too_long_string.as_str(), "invalid@name", "invalid#name", "invalid name", ];
836
837 for name in invalid_names {
838 let result = PackageName::try_from(name);
839 assert!(result.is_err(), "'{}' should be invalid", name);
840
841 assert!(matches!(result.unwrap_err(), PackageNameError::Invalid(_)));
843 }
844 }
845}