1use std::collections::HashMap;
7use std::fs;
8use std::path::Path;
9
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use sha2::{Digest, Sha256};
13use torsh_core::error::{Result, TorshError};
14
15use crate::dependency::{DependencySpec, ResolvedDependency};
16use crate::version::PackageVersion;
17
18pub const LOCKFILE_VERSION: &str = "1.0.0";
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct PackageLockfile {
24 pub version: String,
26 pub package_name: String,
28 pub package_version: String,
30 pub generated_at: DateTime<Utc>,
32 pub dependencies: Vec<LockedDependency>,
34 pub metadata: LockfileMetadata,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct LockedDependency {
41 pub name: String,
43 pub version: String,
45 pub version_req: String,
47 pub integrity: String,
49 pub dependencies: Vec<String>,
51 pub optional: bool,
53 pub platform: Option<String>,
55 pub registry: String,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct LockfileMetadata {
62 pub total_dependencies: usize,
64 pub resolution_strategy: String,
66 pub platform: String,
68 pub generator: String,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct LockedPackage {
75 pub name: String,
77 pub version: String,
79 pub integrity: String,
81 pub dependencies: HashMap<String, String>,
83}
84
85pub struct LockfileGenerator {
87 include_optional: bool,
89 include_platform_specific: bool,
91}
92
93pub struct LockfileValidator {
95 allow_missing_optional: bool,
97 strict_integrity: bool,
99}
100
101impl PackageLockfile {
102 pub fn new(package_name: String, package_version: String) -> Self {
104 Self {
105 version: LOCKFILE_VERSION.to_string(),
106 package_name,
107 package_version,
108 generated_at: Utc::now(),
109 dependencies: Vec::new(),
110 metadata: LockfileMetadata {
111 total_dependencies: 0,
112 resolution_strategy: "highest".to_string(),
113 platform: std::env::consts::OS.to_string(),
114 generator: format!("torsh-package/{}", env!("CARGO_PKG_VERSION")),
115 },
116 }
117 }
118
119 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
121 let contents = fs::read_to_string(path)
122 .map_err(|e| TorshError::InvalidArgument(format!("Failed to read lockfile: {}", e)))?;
123
124 let lockfile: Self = serde_json::from_str(&contents)
125 .map_err(|e| TorshError::InvalidArgument(format!("Failed to parse lockfile: {}", e)))?;
126
127 let lockfile_version = PackageVersion::parse(&lockfile.version)
129 .map_err(|e| TorshError::InvalidArgument(format!("Invalid lockfile version: {}", e)))?;
130
131 let current_version = PackageVersion::parse(LOCKFILE_VERSION)
132 .map_err(|e| TorshError::InvalidArgument(format!("Invalid current version: {}", e)))?;
133
134 if lockfile_version.major != current_version.major {
135 return Err(TorshError::InvalidArgument(format!(
136 "Incompatible lockfile version: {} (expected major version {})",
137 lockfile.version, current_version.major
138 )));
139 }
140
141 Ok(lockfile)
142 }
143
144 pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
146 let contents = serde_json::to_string_pretty(self).map_err(|e| {
147 TorshError::InvalidArgument(format!("Failed to serialize lockfile: {}", e))
148 })?;
149
150 fs::write(path, contents)
151 .map_err(|e| TorshError::InvalidArgument(format!("Failed to write lockfile: {}", e)))?;
152
153 Ok(())
154 }
155
156 pub fn add_dependency(&mut self, dependency: LockedDependency) {
158 self.dependencies.push(dependency);
159 self.metadata.total_dependencies = self.dependencies.len();
160 }
161
162 pub fn get_dependency(&self, name: &str) -> Option<&LockedDependency> {
164 self.dependencies.iter().find(|d| d.name == name)
165 }
166
167 pub fn get_dependency_map(&self) -> HashMap<String, String> {
169 self.dependencies
170 .iter()
171 .map(|d| (d.name.clone(), d.version.clone()))
172 .collect()
173 }
174
175 pub fn is_outdated(&self, days: i64) -> bool {
177 let now = Utc::now();
178 let age = now.signed_duration_since(self.generated_at);
179 age.num_days() > days
180 }
181
182 pub fn get_statistics(&self) -> LockfileStatistics {
184 let mut optional_count = 0;
185 let mut platform_specific_count = 0;
186 let mut total_size = 0;
187
188 for dep in &self.dependencies {
189 if dep.optional {
190 optional_count += 1;
191 }
192 if dep.platform.is_some() {
193 platform_specific_count += 1;
194 }
195 total_size += 1024; }
198
199 LockfileStatistics {
200 total_dependencies: self.dependencies.len(),
201 optional_dependencies: optional_count,
202 platform_specific_dependencies: platform_specific_count,
203 estimated_total_size: total_size,
204 generation_date: self.generated_at,
205 }
206 }
207}
208
209#[derive(Debug, Clone)]
211pub struct LockfileStatistics {
212 pub total_dependencies: usize,
214 pub optional_dependencies: usize,
216 pub platform_specific_dependencies: usize,
218 pub estimated_total_size: u64,
220 pub generation_date: DateTime<Utc>,
222}
223
224impl LockedDependency {
225 pub fn new(name: String, version: String, version_req: String, integrity: String) -> Self {
227 Self {
228 name,
229 version,
230 version_req,
231 integrity,
232 dependencies: Vec::new(),
233 optional: false,
234 platform: None,
235 registry: "https://packages.torsh.rs".to_string(),
236 }
237 }
238
239 pub fn verify_integrity(&self, package_data: &[u8]) -> bool {
241 let computed_hash = Self::compute_integrity(package_data);
242 self.integrity == computed_hash
243 }
244
245 pub fn compute_integrity(data: &[u8]) -> String {
247 let mut hasher = Sha256::new();
248 hasher.update(data);
249 format!("sha256-{}", hex::encode(hasher.finalize()))
250 }
251}
252
253impl Default for LockfileGenerator {
254 fn default() -> Self {
255 Self::new()
256 }
257}
258
259impl LockfileGenerator {
260 pub fn new() -> Self {
262 Self {
263 include_optional: true,
264 include_platform_specific: true,
265 }
266 }
267
268 pub fn with_optional(mut self, include: bool) -> Self {
270 self.include_optional = include;
271 self
272 }
273
274 pub fn with_platform_specific(mut self, include: bool) -> Self {
276 self.include_platform_specific = include;
277 self
278 }
279
280 pub fn generate(
282 &self,
283 package_name: String,
284 package_version: String,
285 resolved: &[ResolvedDependency],
286 ) -> Result<PackageLockfile> {
287 let mut lockfile = PackageLockfile::new(package_name, package_version);
288
289 for dep in resolved {
290 if dep.spec.optional && !self.include_optional {
292 continue;
293 }
294
295 if dep.spec.platform.is_some() && !self.include_platform_specific {
297 continue;
298 }
299
300 let locked_dep = LockedDependency {
301 name: dep.spec.name.clone(),
302 version: dep.resolved_version.clone(),
303 version_req: dep.spec.version_req.clone(),
304 integrity: "sha256-placeholder".to_string(), dependencies: dep
306 .dependencies
307 .iter()
308 .map(|d| d.spec.name.clone())
309 .collect(),
310 optional: dep.spec.optional,
311 platform: dep.spec.platform.clone(),
312 registry: "https://packages.torsh.rs".to_string(),
313 };
314
315 lockfile.add_dependency(locked_dep);
316 }
317
318 Ok(lockfile)
319 }
320
321 pub fn generate_from_specs(
323 &self,
324 package_name: String,
325 package_version: String,
326 specs: &[DependencySpec],
327 ) -> Result<PackageLockfile> {
328 let resolved: Vec<_> = specs
330 .iter()
331 .map(|spec| ResolvedDependency {
332 spec: spec.clone(),
333 resolved_version: "0.0.0".to_string(), package_path: None,
335 dependencies: Vec::new(),
336 })
337 .collect();
338
339 self.generate(package_name, package_version, &resolved)
340 }
341}
342
343impl Default for LockfileValidator {
344 fn default() -> Self {
345 Self::new()
346 }
347}
348
349impl LockfileValidator {
350 pub fn new() -> Self {
352 Self {
353 allow_missing_optional: true,
354 strict_integrity: true,
355 }
356 }
357
358 pub fn with_allow_missing_optional(mut self, allow: bool) -> Self {
360 self.allow_missing_optional = allow;
361 self
362 }
363
364 pub fn with_strict_integrity(mut self, strict: bool) -> Self {
366 self.strict_integrity = strict;
367 self
368 }
369
370 pub fn validate(&self, lockfile: &PackageLockfile) -> Result<ValidationReport> {
372 let mut report = ValidationReport::new();
373
374 let lockfile_version = PackageVersion::parse(&lockfile.version)
376 .map_err(|e| TorshError::InvalidArgument(format!("Invalid lockfile version: {}", e)))?;
377
378 let current_version = PackageVersion::parse(LOCKFILE_VERSION)
379 .map_err(|e| TorshError::InvalidArgument(format!("Invalid current version: {}", e)))?;
380
381 if lockfile_version.major != current_version.major {
382 report.add_error(format!(
383 "Incompatible lockfile version: {} (expected major version {})",
384 lockfile.version, current_version.major
385 ));
386 }
387
388 let mut seen = HashMap::new();
390 for dep in &lockfile.dependencies {
391 if let Some(existing) = seen.insert(&dep.name, &dep.version) {
392 report.add_warning(format!(
393 "Duplicate dependency: {} (versions {} and {})",
394 dep.name, existing, dep.version
395 ));
396 }
397 }
398
399 for dep in &lockfile.dependencies {
401 self.validate_dependency(dep, &mut report)?;
402 }
403
404 if let Err(e) = self.check_circular_dependencies(lockfile) {
406 report.add_error(format!("Circular dependency detected: {}", e));
407 }
408
409 Ok(report)
410 }
411
412 fn validate_dependency(
414 &self,
415 dep: &LockedDependency,
416 report: &mut ValidationReport,
417 ) -> Result<()> {
418 if PackageVersion::parse(&dep.version).is_err() {
420 report.add_error(format!("Invalid version for {}: {}", dep.name, dep.version));
421 }
422
423 if self.strict_integrity && !dep.integrity.starts_with("sha256-") {
425 report.add_error(format!(
426 "Invalid integrity hash format for {}: {}",
427 dep.name, dep.integrity
428 ));
429 }
430
431 if dep.optional && !self.allow_missing_optional {
433 report.add_warning(format!(
434 "Optional dependency {} may not be installed",
435 dep.name
436 ));
437 }
438
439 Ok(())
440 }
441
442 fn check_circular_dependencies(&self, lockfile: &PackageLockfile) -> Result<()> {
444 let mut graph: HashMap<String, Vec<String>> = HashMap::new();
445
446 for dep in &lockfile.dependencies {
447 graph.insert(dep.name.clone(), dep.dependencies.clone());
448 }
449
450 let mut visited = HashSet::new();
452 let mut rec_stack = HashSet::new();
453
454 for dep in &lockfile.dependencies {
455 if !visited.contains(&dep.name) {
456 if self.has_cycle(&dep.name, &graph, &mut visited, &mut rec_stack)? {
457 return Err(TorshError::InvalidArgument(format!(
458 "Circular dependency involving: {}",
459 dep.name
460 )));
461 }
462 }
463 }
464
465 Ok(())
466 }
467
468 fn has_cycle(
470 &self,
471 node: &str,
472 graph: &HashMap<String, Vec<String>>,
473 visited: &mut HashSet<String>,
474 rec_stack: &mut HashSet<String>,
475 ) -> Result<bool> {
476 visited.insert(node.to_string());
477 rec_stack.insert(node.to_string());
478
479 if let Some(neighbors) = graph.get(node) {
480 for neighbor in neighbors {
481 if !visited.contains(neighbor) {
482 if self.has_cycle(neighbor, graph, visited, rec_stack)? {
483 return Ok(true);
484 }
485 } else if rec_stack.contains(neighbor) {
486 return Ok(true);
487 }
488 }
489 }
490
491 rec_stack.remove(node);
492 Ok(false)
493 }
494
495 pub fn compare_lockfiles(&self, old: &PackageLockfile, new: &PackageLockfile) -> LockfileDiff {
497 let mut diff = LockfileDiff::new();
498
499 let old_map: HashMap<_, _> = old
500 .dependencies
501 .iter()
502 .map(|d| (&d.name, &d.version))
503 .collect();
504
505 let new_map: HashMap<_, _> = new
506 .dependencies
507 .iter()
508 .map(|d| (&d.name, &d.version))
509 .collect();
510
511 for (name, new_version) in &new_map {
513 if let Some(old_version) = old_map.get(name) {
514 if old_version != new_version {
515 diff.updated.push((
516 name.to_string(),
517 old_version.to_string(),
518 new_version.to_string(),
519 ));
520 }
521 } else {
522 diff.added.push((name.to_string(), new_version.to_string()));
523 }
524 }
525
526 for (name, old_version) in &old_map {
527 if !new_map.contains_key(name) {
528 diff.removed
529 .push((name.to_string(), old_version.to_string()));
530 }
531 }
532
533 diff
534 }
535}
536
537use std::collections::HashSet;
538
539#[derive(Debug, Clone)]
541pub struct ValidationReport {
542 pub errors: Vec<String>,
544 pub warnings: Vec<String>,
546}
547
548impl ValidationReport {
549 pub fn new() -> Self {
551 Self {
552 errors: Vec::new(),
553 warnings: Vec::new(),
554 }
555 }
556
557 pub fn add_error(&mut self, error: String) {
559 self.errors.push(error);
560 }
561
562 pub fn add_warning(&mut self, warning: String) {
564 self.warnings.push(warning);
565 }
566
567 pub fn is_valid(&self) -> bool {
569 self.errors.is_empty()
570 }
571
572 pub fn summary(&self) -> String {
574 format!(
575 "Validation: {} errors, {} warnings",
576 self.errors.len(),
577 self.warnings.len()
578 )
579 }
580}
581
582impl Default for ValidationReport {
583 fn default() -> Self {
584 Self::new()
585 }
586}
587
588#[derive(Debug, Clone)]
590pub struct LockfileDiff {
591 pub added: Vec<(String, String)>,
593 pub removed: Vec<(String, String)>,
595 pub updated: Vec<(String, String, String)>,
597}
598
599impl LockfileDiff {
600 pub fn new() -> Self {
602 Self {
603 added: Vec::new(),
604 removed: Vec::new(),
605 updated: Vec::new(),
606 }
607 }
608
609 pub fn has_changes(&self) -> bool {
611 !self.added.is_empty() || !self.removed.is_empty() || !self.updated.is_empty()
612 }
613
614 pub fn summary(&self) -> String {
616 format!(
617 "{} added, {} removed, {} updated",
618 self.added.len(),
619 self.removed.len(),
620 self.updated.len()
621 )
622 }
623}
624
625impl Default for LockfileDiff {
626 fn default() -> Self {
627 Self::new()
628 }
629}
630
631#[cfg(test)]
632mod tests {
633 use super::*;
634
635 #[test]
636 fn test_lockfile_creation() {
637 let lockfile = PackageLockfile::new("test-package".to_string(), "1.0.0".to_string());
638 assert_eq!(lockfile.package_name, "test-package");
639 assert_eq!(lockfile.package_version, "1.0.0");
640 assert_eq!(lockfile.version, LOCKFILE_VERSION);
641 }
642
643 #[test]
644 fn test_locked_dependency() {
645 let dep = LockedDependency::new(
646 "test-dep".to_string(),
647 "1.0.0".to_string(),
648 "^1.0.0".to_string(),
649 "sha256-abc123".to_string(),
650 );
651
652 assert_eq!(dep.name, "test-dep");
653 assert_eq!(dep.version, "1.0.0");
654 assert_eq!(dep.integrity, "sha256-abc123");
655 }
656
657 #[test]
658 fn test_integrity_verification() {
659 let data = b"test package data";
660 let integrity = LockedDependency::compute_integrity(data);
661
662 assert!(integrity.starts_with("sha256-"));
663
664 let dep = LockedDependency::new(
665 "test".to_string(),
666 "1.0.0".to_string(),
667 "^1.0.0".to_string(),
668 integrity,
669 );
670
671 assert!(dep.verify_integrity(data));
672 assert!(!dep.verify_integrity(b"different data"));
673 }
674
675 #[test]
676 fn test_lockfile_generator() {
677 let generator = LockfileGenerator::new().with_optional(true);
678
679 let specs = vec![DependencySpec::new(
680 "test-dep".to_string(),
681 "^1.0.0".to_string(),
682 )];
683
684 let lockfile = generator
685 .generate_from_specs("test-package".to_string(), "1.0.0".to_string(), &specs)
686 .unwrap();
687
688 assert_eq!(lockfile.dependencies.len(), 1);
689 assert_eq!(lockfile.dependencies[0].name, "test-dep");
690 }
691
692 #[test]
693 fn test_lockfile_validator() {
694 let mut lockfile = PackageLockfile::new("test-package".to_string(), "1.0.0".to_string());
695
696 let dep = LockedDependency::new(
697 "test-dep".to_string(),
698 "1.0.0".to_string(),
699 "^1.0.0".to_string(),
700 "sha256-abc123".to_string(),
701 );
702
703 lockfile.add_dependency(dep);
704
705 let validator = LockfileValidator::new();
706 let report = validator.validate(&lockfile).unwrap();
707
708 assert!(report.is_valid());
709 }
710
711 #[test]
712 fn test_lockfile_diff() {
713 let mut old = PackageLockfile::new("test-package".to_string(), "1.0.0".to_string());
714 old.add_dependency(LockedDependency::new(
715 "dep1".to_string(),
716 "1.0.0".to_string(),
717 "^1.0.0".to_string(),
718 "sha256-abc".to_string(),
719 ));
720
721 let mut new = PackageLockfile::new("test-package".to_string(), "1.0.0".to_string());
722 new.add_dependency(LockedDependency::new(
723 "dep1".to_string(),
724 "1.1.0".to_string(),
725 "^1.0.0".to_string(),
726 "sha256-def".to_string(),
727 ));
728 new.add_dependency(LockedDependency::new(
729 "dep2".to_string(),
730 "2.0.0".to_string(),
731 "^2.0.0".to_string(),
732 "sha256-ghi".to_string(),
733 ));
734
735 let validator = LockfileValidator::new();
736 let diff = validator.compare_lockfiles(&old, &new);
737
738 assert_eq!(diff.updated.len(), 1);
739 assert_eq!(diff.added.len(), 1);
740 assert!(diff.has_changes());
741 }
742}