1use crate::fs_beneath::ObjectId;
18use crate::subvolume_error::MigrateError;
19use std::io::Write;
20use std::os::unix::fs::OpenOptionsExt;
21use std::os::unix::io::AsRawFd;
22use std::path::{Path, PathBuf};
23
24pub const JOURNAL_NAME: &str = "cli-git-convert";
26
27pub const LOCK_NAME: &str = "cli-git-convert.lock";
30
31pub const CLAIM_PREFIX: &str = "cli-git-convert.owner";
33
34const SMALL_FILE_LIMIT: u64 = 1 << 20;
36
37const JOURNAL_HEADER: &str = "cli-git-convert v1";
39
40#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
50pub enum Phase {
51 Started,
54 Creating,
56 Synced,
58 Exchanged,
60 Refreshed,
62 Removed,
64}
65
66impl Phase {
67 fn name(self) -> &'static str {
69 return match self {
70 Phase::Started => "started",
71 Phase::Creating => "creating",
72 Phase::Synced => "synced",
73 Phase::Exchanged => "exchanged",
74 Phase::Refreshed => "refreshed",
75 Phase::Removed => "removed",
76 };
77 }
78
79 fn parse(text: &str) -> Option<Phase> {
81 return [Phase::Started, Phase::Creating, Phase::Synced, Phase::Exchanged, Phase::Refreshed, Phase::Removed]
82 .into_iter()
83 .find(|phase: &Phase| { return phase.name() == text; });
84 }
85}
86
87#[derive(Clone, Debug, Eq, PartialEq)]
97pub struct Journal {
98 pub phase: Phase,
100 pub token: String,
102 pub path: Vec<u8>,
104 pub old_identity: ObjectId,
107 pub git_file: Vec<u8>,
109 pub new_identity: Option<ObjectId>,
112}
113
114fn hex(bytes: &[u8]) -> String {
116 return bytes.iter().map(|byte: &u8| { return format!("{byte:02x}"); }).collect();
117}
118
119fn unhex(text: &str) -> Option<Vec<u8>> {
121 if text.len() % 2 != 0 {
122 return None;
123 }
124 return (0..text.len()).step_by(2).map(|at: usize| { return u8::from_str_radix(text.get(at..at + 2)?, 16).ok(); }).collect();
125}
126
127fn temporary_name() -> String {
129 let nanos: u128 = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |elapsed: std::time::Duration| { return elapsed.as_nanos(); });
130 return format!("{JOURNAL_NAME}.tmp.{}.{nanos}", std::process::id());
131}
132
133impl Journal {
134 fn encode(&self) -> String {
136 return format!(
137 "{JOURNAL_HEADER}\nphase={}\ntoken={}\npath={}\nold_identity={}\ngit_file={}\nnew_identity={}\n",
138 self.phase.name(),
139 self.token,
140 hex(self.path.as_slice()),
141 self.old_identity.encode(),
142 hex(self.git_file.as_slice()),
143 self.new_identity.map_or_else(|| { return String::from("-"); }, |identity: ObjectId| { return identity.encode(); })
144 );
145 }
146
147 fn decode(text: &str) -> Result<Journal, MigrateError> {
149 let bad = || { return MigrateError::Journal(String::from("the conversion journal has a shape this command does not know; nothing was changed")); };
150 let mut lines = text.lines();
151 if lines.next() != Some(JOURNAL_HEADER) {
152 return Err(bad());
153 }
154 let field = |name: &str| -> Option<String> { return text.lines().find_map(|line: &str| { return line.strip_prefix(name).and_then(|rest: &str| { return rest.strip_prefix('='); }).map(String::from); }); };
155 let new_identity: Option<ObjectId> = match field("new_identity").as_deref() {
156 None | Some("-") => None,
157 Some(encoded) => Some(ObjectId::decode(encoded).ok_or_else(bad)?),
158 };
159 let old_identity: ObjectId = match field("old_identity") {
161 Some(encoded) => ObjectId::decode(encoded.as_str()).ok_or_else(bad)?,
162 None => ObjectId {
163 device: field("old_device").and_then(|digits: String| { return digits.parse().ok(); }).ok_or_else(bad)?,
164 inode: field("old_inode").and_then(|digits: String| { return digits.parse().ok(); }).ok_or_else(bad)?,
165 birth: None,
166 },
167 };
168 return Ok(Journal {
169 phase: Phase::parse(field("phase").ok_or_else(bad)?.as_str()).ok_or_else(bad)?,
170 token: field("token").ok_or_else(bad)?,
171 path: unhex(field("path").ok_or_else(bad)?.as_str()).ok_or_else(bad)?,
172 old_identity,
173 git_file: unhex(field("git_file").ok_or_else(bad)?.as_str()).ok_or_else(bad)?,
174 new_identity,
175 });
176 }
177
178 fn write_temporary(&self, admin: &Path) -> std::io::Result<PathBuf> {
180 let temporary: PathBuf = admin.join(temporary_name());
181 let mut file: std::fs::File = std::fs::OpenOptions::new().write(true).create_new(true).mode(0o600).open(&temporary)?;
182 let written: std::io::Result<()> = file.write_all(self.encode().as_bytes()).and_then(|()| { return file.sync_all(); });
183 if let Err(error) = written {
184 let _removed: std::io::Result<()> = std::fs::remove_file(&temporary);
185 return Err(error);
186 }
187 return Ok(temporary);
188 }
189
190 pub fn save(&self, admin: &Path) -> Result<(), MigrateError> {
192 let target: PathBuf = admin.join(JOURNAL_NAME);
193 let written: std::io::Result<()> = (|| {
194 let temporary: PathBuf = self.write_temporary(admin)?;
195 if let Err(error) = std::fs::rename(&temporary, &target) {
196 let _removed: std::io::Result<()> = std::fs::remove_file(&temporary);
197 return Err(error);
198 }
199 return std::fs::File::open(admin).and_then(|directory: std::fs::File| { return directory.sync_all(); });
200 })();
201 return written.map_err(|error: std::io::Error| { return MigrateError::io("write the conversion journal", target.as_path(), &error); });
202 }
203
204 pub fn create_new(&self, admin: &Path) -> Result<(), MigrateError> {
207 let target: PathBuf = admin.join(JOURNAL_NAME);
208 let written: std::io::Result<()> = (|| {
209 let temporary: PathBuf = self.write_temporary(admin)?;
210 let linked: std::io::Result<()> = std::fs::hard_link(&temporary, &target);
211 let _removed: std::io::Result<()> = std::fs::remove_file(&temporary);
212 linked?;
213 return std::fs::File::open(admin).and_then(|directory: std::fs::File| { return directory.sync_all(); });
214 })();
215 return written.map_err(|error: std::io::Error| { return MigrateError::io("create the conversion journal", target.as_path(), &error); });
216 }
217
218 pub fn load(admin: &Path) -> Result<Option<Journal>, MigrateError> {
220 let path: PathBuf = admin.join(JOURNAL_NAME);
221 match read_plain_file(path.as_path(), SMALL_FILE_LIMIT) {
222 Ok(bytes) => return Journal::decode(String::from_utf8_lossy(bytes.as_slice()).as_ref()).map(Some),
223 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
224 Err(error) => return Err(MigrateError::io("read the conversion journal", path.as_path(), &error)),
225 }
226 }
227}
228
229pub fn read_plain_file(path: &Path, limit: u64) -> std::io::Result<Vec<u8>> {
241 let file: std::fs::File = std::fs::OpenOptions::new().read(true).custom_flags(libc::O_NONBLOCK | libc::O_NOFOLLOW | libc::O_CLOEXEC).open(path)?;
242 if !file.metadata()?.file_type().is_file() {
243 return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "it is not a plain file (a named pipe, a device or a directory)"));
244 }
245 let mut bytes: Vec<u8> = Vec::new();
246 std::io::Read::read_to_end(&mut std::io::Read::take(&file, limit + 1), &mut bytes)?;
247 if bytes.len() as u64 > limit {
248 return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "it is larger than this command reads"));
249 }
250 return Ok(bytes);
251}
252
253pub fn read_plain_text(path: &Path) -> std::io::Result<String> {
255 return String::from_utf8(read_plain_file(path, SMALL_FILE_LIMIT)?).map_err(|_error| { return std::io::Error::new(std::io::ErrorKind::InvalidData, "it is not text"); });
256}
257
258pub fn is_recorded_object(recorded: &ObjectId, found: &ObjectId) -> bool {
261 return recorded.device == found.device && recorded.inode == found.inode && (recorded.birth.is_none() || recorded.birth == found.birth);
262}
263
264pub fn legacy_lock_is_held(admin: &Path) -> Result<bool, MigrateError> {
279 let path: PathBuf = admin.join(LOCK_NAME);
280 let file: std::fs::File = match std::fs::OpenOptions::new().read(true).write(true).custom_flags(libc::O_NONBLOCK | libc::O_NOFOLLOW | libc::O_CLOEXEC).open(&path) {
281 Ok(opened) => opened,
282 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false),
283 Err(error) => return Err(MigrateError::io("open the conversion lock an older release left", path.as_path(), &error)),
284 };
285 let plain: bool = file.metadata().map_err(|error: std::io::Error| { return MigrateError::io("examine the conversion lock an older release left", path.as_path(), &error); })?.file_type().is_file();
286 if !plain {
287 return Ok(false);
288 }
289 let result: libc::c_int = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
291 if result != 0 {
292 let error: std::io::Error = std::io::Error::last_os_error();
293 if error.kind() == std::io::ErrorKind::WouldBlock {
294 return Ok(true);
295 }
296 return Err(MigrateError::io("probe the conversion lock an older release left", path.as_path(), &error));
297 }
298 return match std::fs::remove_file(&path) {
300 Ok(()) => Ok(false),
301 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
302 Err(error) => Err(MigrateError::io("remove the conversion lock an older release left", path.as_path(), &error)),
303 };
304}