1use crate::{local::Local, util, verify_package_digest, Error, Result};
2use crev_data::{proof, Digest};
3use serde::{Deserialize, Serialize};
4
5use std::{
6 collections::HashSet,
7 fs,
8 path::{Path, PathBuf},
9};
10
11pub mod staging;
12
13#[derive(Serialize, Deserialize, Debug, Clone)]
14pub struct PackageConfig {
15 pub version: u64,
16 #[serde(rename = "trust-root")]
17 pub trust_root: String,
18}
19
20const CREV_DOT_NAME: &str = ".crev";
21
22#[derive(thiserror::Error, Debug)]
23#[error("Package config not-initialized. Use `crev package init` to generate it.")]
24pub struct PackageDirNotFound;
25
26fn find_package_root_dir() -> std::result::Result<PathBuf, PackageDirNotFound> {
27 let path = Path::new(".")
28 .canonicalize()
29 .map_err(|_| PackageDirNotFound)?;
30 let mut path = path.as_path();
31 loop {
32 if path.join(CREV_DOT_NAME).is_dir() {
33 return Ok(path.to_owned());
34 }
35 path = path.parent().ok_or(PackageDirNotFound)?;
36 }
37}
38
39pub struct Repo {
44 root_dir: PathBuf,
46 staging: Option<staging::Staging>,
48}
49
50impl Repo {
51 pub fn init(path: &Path, id_str: String) -> Result<Self> {
52 let repo = Self::new(path)?;
53
54 fs::create_dir_all(repo.dot_crev_path())?;
55
56 let config_path = repo.package_config_path();
57 if config_path.exists() {
58 return Err(Error::PathAlreadyExists(config_path.as_path().into()));
59 }
60 util::store_to_file_with(&config_path, move |w| {
61 serde_yaml::to_writer(
62 w,
63 &PackageConfig {
64 version: 0,
65 trust_root: id_str.clone(),
66 },
67 )
68 })??;
69
70 Ok(repo)
71 }
72
73 pub fn open(path: &Path) -> Result<Self> {
74 if !path.exists() {
75 return Err(
76 std::io::Error::new(std::io::ErrorKind::NotFound, "directory not found").into(),
77 );
78 }
79
80 Self::new(path)
81 }
82
83 pub fn auto_open() -> Result<Self> {
84 let root_path = find_package_root_dir()?;
85 Self::open(&root_path)
86 }
87
88 #[allow(clippy::new_ret_no_self)]
89 fn new(root_dir: &Path) -> Result<Self> {
90 let root_dir = root_dir.canonicalize()?;
91 Ok(Self {
92 root_dir,
93 staging: None,
94 })
95 }
96
97 fn package_config_path(&self) -> PathBuf {
98 self.dot_crev_path().join("config.yaml")
99 }
100
101 pub fn load_package_config(&self) -> Result<PackageConfig> {
102 let config = self.try_load_package_config()?;
103 config.ok_or(Error::PackageConfigNotInitialized)
104 }
105
106 pub fn try_load_package_config(&self) -> Result<Option<PackageConfig>> {
107 let path = self.package_config_path();
108
109 if !path.exists() {
110 return Ok(None);
111 }
112 let config_str = fs::read_to_string(&path)?;
113
114 Ok(Some(serde_yaml::from_str(&config_str)?))
115 }
116
117 #[must_use]
118 pub fn dot_crev_path(&self) -> PathBuf {
119 self.root_dir.join(CREV_DOT_NAME)
120 }
121
122 pub fn staging(&mut self) -> Result<&mut staging::Staging> {
123 if self.staging.is_none() {
124 self.staging = Some(staging::Staging::open(&self.root_dir)?);
125 }
126 Ok(self.staging.as_mut().unwrap())
127 }
128
129 #[must_use]
130 pub fn get_proof_rel_store_path(&self, _proof: &proof::Proof) -> PathBuf {
131 unimplemented!();
132 }
133
134 pub fn package_verify(
135 &mut self,
136 local: &Local,
137 allow_dirty: bool,
138 for_id: Option<String>,
139 params: &crate::TrustDistanceParams,
140 requirements: &crate::VerificationRequirements,
141 ) -> Result<crate::VerificationStatus> {
142 if !allow_dirty && self.is_unclean()? {
143 return Err(Error::GitRepositoryIsNotInACleanState);
144 }
145
146 let db = local.load_db()?;
147 let trust_set = local.trust_set_for_id(for_id.as_deref(), params, &db)?;
148 let ignore_list = fnv::FnvHashSet::default();
149 let digest = crate::get_recursive_digest_for_git_dir(&self.root_dir, &ignore_list)?;
150 Ok(verify_package_digest(
151 &digest,
152 &trust_set,
153 requirements,
154 &db,
155 ))
156 }
157
158 pub fn package_digest(&mut self, allow_dirty: bool) -> Result<Digest> {
159 if !allow_dirty && self.is_unclean()? {
160 return Err(Error::GitRepositoryIsNotInACleanState);
161 }
162
163 let ignore_list = HashSet::default();
164 crate::get_recursive_digest_for_git_dir(&self.root_dir, &ignore_list)
165 }
166
167 fn is_unclean(&self) -> Result<bool> {
168 let git_repo = git2::Repository::open(&self.root_dir)?;
169 if git_repo.state() != git2::RepositoryState::Clean {
170 return Err(Error::GitRepositoryIsNotInACleanState);
171 }
172 let mut status_opts = git2::StatusOptions::new();
173 status_opts.include_unmodified(true);
174 status_opts.include_untracked(false);
175 let mut unclean_found = false;
176 for entry in git_repo.statuses(Some(&mut status_opts))?.iter() {
177 if entry.status() != git2::Status::CURRENT {
178 unclean_found = true;
179 }
180 }
181
182 Ok(unclean_found)
183 }
184
185 pub fn status(&mut self) -> Result<()> {
187 let staging = self.staging()?;
188 for (k, _v) in &staging.entries {
189 log::info!("{}", k.display());
190 }
191
192 Ok(())
193 }
194
195 pub fn add(&mut self, file_paths: Vec<PathBuf>) -> Result<()> {
196 let staging = self.staging()?;
197 for path in file_paths {
198 staging.insert(&path)?;
199 }
200 staging.save()?;
201
202 Ok(())
203 }
204
205 pub fn remove(&mut self, file_paths: Vec<PathBuf>) -> Result<()> {
206 let staging = self.staging()?;
207 for path in file_paths {
208 staging.remove(&path)?;
209 }
210 staging.save()?;
211
212 Ok(())
213 }
214}