1use super::file_kind::BufferFileKind;
8use super::format::{self, BufferFormat};
9use super::persistence::Persistence;
10use crate::model::encoding::Encoding;
11use crate::model::filesystem::{FileMetadata, FileSystem, FileWriter, WriteOp};
12use crate::model::piece_tree::{BufferData, BufferLocation, PieceTree, StringBuffer};
13use std::io::{self, Write};
14use std::path::{Path, PathBuf};
15use std::sync::Arc;
16
17#[derive(Debug, Clone, PartialEq)]
26pub struct SudoSaveRequired {
27 pub temp_path: PathBuf,
29 pub dest_path: PathBuf,
31 pub uid: u32,
33 pub gid: u32,
35 pub mode: u32,
37}
38
39impl std::fmt::Display for SudoSaveRequired {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 write!(
42 f,
43 "Permission denied saving to {}. Use sudo to complete the operation.",
44 self.dest_path.display()
45 )
46 }
47}
48
49impl std::error::Error for SudoSaveRequired {}
50
51pub(crate) struct WriteRecipe {
57 pub(crate) src_path: Option<PathBuf>,
59 pub(crate) insert_data: Vec<Vec<u8>>,
61 pub(crate) actions: Vec<RecipeAction>,
63}
64
65#[derive(Debug, Clone, Copy)]
67pub(crate) enum RecipeAction {
68 Copy { offset: u64, len: u64 },
70 Insert { index: usize },
72}
73
74impl WriteRecipe {
75 pub(crate) fn to_write_ops(&self) -> Vec<WriteOp<'_>> {
77 self.actions
78 .iter()
79 .map(|action| match action {
80 RecipeAction::Copy { offset, len } => WriteOp::Copy {
81 offset: *offset,
82 len: *len,
83 },
84 RecipeAction::Insert { index } => WriteOp::Insert {
85 data: &self.insert_data[*index],
86 },
87 })
88 .collect()
89 }
90
91 pub(crate) fn has_copy_ops(&self) -> bool {
93 self.actions
94 .iter()
95 .any(|a| matches!(a, RecipeAction::Copy { .. }))
96 }
97
98 pub(crate) fn flatten_inserts(&self) -> Vec<u8> {
101 let mut result = Vec::new();
102 for action in &self.actions {
103 if let RecipeAction::Insert { index } = action {
104 result.extend_from_slice(&self.insert_data[*index]);
105 }
106 }
107 result
108 }
109}
110
111pub(super) fn should_use_inplace_write(
122 fs: &Arc<dyn FileSystem + Send + Sync>,
123 dest_path: &Path,
124) -> bool {
125 !fs.is_owner(dest_path)
126}
127
128pub(super) fn build_write_recipe(
137 piece_tree: &PieceTree,
138 buffers: &[StringBuffer],
139 format: &BufferFormat,
140 file_kind: &BufferFileKind,
141 persistence: &Persistence,
142) -> io::Result<WriteRecipe> {
143 let total = piece_tree.total_bytes();
144
145 let needs_line_ending_conversion = format.line_ending_changed_since_load();
152 let needs_encoding_conversion = !file_kind.is_binary()
158 && (format.encoding_changed_since_load()
159 || !matches!(format.encoding(), Encoding::Utf8 | Encoding::Ascii));
160 let needs_conversion = needs_line_ending_conversion || needs_encoding_conversion;
161
162 let src_path_for_copy: Option<&Path> = if needs_conversion {
163 None
164 } else {
165 persistence
166 .file_path()
167 .filter(|p| persistence.fs().exists(p))
168 };
169 let target_ending = format.line_ending();
170 let target_encoding = format.encoding();
171
172 let mut insert_data: Vec<Vec<u8>> = Vec::new();
173 let mut actions: Vec<RecipeAction> = Vec::new();
174
175 if let Some(bom) = target_encoding.bom_bytes() {
177 insert_data.push(bom.to_vec());
178 actions.push(RecipeAction::Insert { index: 0 });
179 }
180
181 for piece_view in piece_tree.iter_pieces_in_range(0, total) {
182 let buffer_id = piece_view.location.buffer_id();
183 let buffer = buffers.get(buffer_id).ok_or_else(|| {
184 io::Error::new(
185 io::ErrorKind::InvalidData,
186 format!("Buffer {} not found", buffer_id),
187 )
188 })?;
189
190 match &buffer.data {
191 BufferData::Unloaded {
193 file_path,
194 file_offset,
195 ..
196 } => {
197 let can_copy = matches!(piece_view.location, BufferLocation::Stored(_))
203 && src_path_for_copy.is_some_and(|src| file_path == src);
204
205 if can_copy {
206 let src_offset = (*file_offset + piece_view.buffer_offset) as u64;
207 actions.push(RecipeAction::Copy {
208 offset: src_offset,
209 len: piece_view.bytes as u64,
210 });
211 continue;
212 }
213
214 let data = persistence.fs().read_range(
217 file_path,
218 (*file_offset + piece_view.buffer_offset) as u64,
219 piece_view.bytes,
220 )?;
221
222 let data = if needs_line_ending_conversion {
223 format::convert_line_endings_to(&data, target_ending)
224 } else {
225 data
226 };
227
228 let data = if needs_encoding_conversion {
230 format::convert_to_encoding(&data, target_encoding)
231 } else {
232 data
233 };
234
235 let index = insert_data.len();
236 insert_data.push(data);
237 actions.push(RecipeAction::Insert { index });
238 }
239
240 BufferData::Loaded { data, .. } => {
242 let start = piece_view.buffer_offset;
243 let end = start + piece_view.bytes;
244 let chunk = &data[start..end];
245
246 let chunk = if needs_line_ending_conversion {
247 format::convert_line_endings_to(chunk, target_ending)
248 } else {
249 chunk.to_vec()
250 };
251
252 let chunk = if needs_encoding_conversion {
254 format::convert_to_encoding(&chunk, target_encoding)
255 } else {
256 chunk
257 };
258
259 let index = insert_data.len();
260 insert_data.push(chunk);
261 actions.push(RecipeAction::Insert { index });
262 }
263 }
264 }
265
266 Ok(WriteRecipe {
267 src_path: src_path_for_copy.map(|p| p.to_path_buf()),
268 insert_data,
269 actions,
270 })
271}
272
273pub(super) fn create_temp_file(
279 fs: &Arc<dyn FileSystem + Send + Sync>,
280 dest_path: &Path,
281) -> io::Result<(PathBuf, Box<dyn FileWriter>)> {
282 let same_dir_temp = fs.temp_path_for(dest_path);
284 match fs.create_file(&same_dir_temp) {
285 Ok(file) => Ok((same_dir_temp, file)),
286 Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
287 let temp_path = fs.unique_temp_path(dest_path);
289 let file = fs.create_file(&temp_path)?;
290 Ok((temp_path, file))
291 }
292 Err(e) => Err(e),
293 }
294}
295
296pub(super) fn create_recovery_temp_file(
299 fs: &Arc<dyn FileSystem + Send + Sync>,
300 dest_path: &Path,
301) -> io::Result<(PathBuf, Box<dyn FileWriter>)> {
302 let recovery_dir = crate::input::input_history::get_data_dir()
304 .map(|d| d.join("recovery"))
305 .unwrap_or_else(|_| std::env::temp_dir());
306
307 fs.create_dir_all(&recovery_dir)?;
309
310 let file_name = dest_path
312 .file_name()
313 .unwrap_or_else(|| std::ffi::OsStr::new("fresh-save"));
314 let timestamp = std::time::SystemTime::now()
315 .duration_since(std::time::UNIX_EPOCH)
316 .map(|d| d.as_nanos())
317 .unwrap_or(0);
318 let pid = std::process::id();
319
320 let temp_name = format!(
321 ".inplace-{}-{}-{}.tmp",
322 file_name.to_string_lossy(),
323 pid,
324 timestamp
325 );
326 let temp_path = recovery_dir.join(temp_name);
327
328 let file = fs.create_file(&temp_path)?;
329 Ok((temp_path, file))
330}
331
332pub(super) fn inplace_recovery_meta_path(dest_path: &Path) -> PathBuf {
335 let recovery_dir = crate::input::input_history::get_data_dir()
336 .map(|d| d.join("recovery"))
337 .unwrap_or_else(|_| std::env::temp_dir());
338
339 let hash = crate::services::recovery::path_hash(dest_path);
340 recovery_dir.join(format!("{}.inplace.json", hash))
341}
342
343pub(super) fn write_inplace_recovery_meta(
346 fs: &Arc<dyn FileSystem + Send + Sync>,
347 meta_path: &Path,
348 dest_path: &Path,
349 temp_path: &Path,
350 original_metadata: &Option<FileMetadata>,
351) -> io::Result<()> {
352 #[cfg(unix)]
353 let (uid, gid, mode) = original_metadata
354 .as_ref()
355 .map(|m| {
356 (
357 m.uid.unwrap_or(0),
358 m.gid.unwrap_or(0),
359 m.permissions.as_ref().map(|p| p.mode()).unwrap_or(0o644),
360 )
361 })
362 .unwrap_or((0, 0, 0o644));
363 #[cfg(not(unix))]
364 let (uid, gid, mode) = (0u32, 0u32, 0o644u32);
365
366 let recovery = crate::services::recovery::InplaceWriteRecovery::new(
367 dest_path.to_path_buf(),
368 temp_path.to_path_buf(),
369 uid,
370 gid,
371 mode,
372 );
373
374 let json = serde_json::to_string_pretty(&recovery)
375 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
376
377 fs.write_file(meta_path, json.as_bytes())
378}
379
380pub(super) fn save_with_inplace_write(
393 fs: &Arc<dyn FileSystem + Send + Sync>,
394 dest_path: &Path,
395 recipe: &WriteRecipe,
396) -> anyhow::Result<()> {
397 let original_metadata = fs.metadata_if_exists(dest_path);
398
399 if !recipe.has_copy_ops() {
402 let data = recipe.flatten_inserts();
403 return write_data_inplace(fs, dest_path, &data, original_metadata);
404 }
405
406 let (temp_path, mut temp_file) = create_recovery_temp_file(fs, dest_path)?;
410 if let Err(e) = write_recipe_to_file(fs, &mut temp_file, recipe) {
411 #[allow(clippy::let_underscore_must_use)]
413 let _ = fs.remove_file(&temp_path);
414 return Err(e.into());
415 }
416 temp_file.sync_all()?;
417 drop(temp_file);
418
419 let recovery_meta_path = inplace_recovery_meta_path(dest_path);
422 #[allow(clippy::let_underscore_must_use)]
424 let _ = write_inplace_recovery_meta(
425 fs,
426 &recovery_meta_path,
427 dest_path,
428 &temp_path,
429 &original_metadata,
430 );
431
432 match fs.open_file_for_write(dest_path) {
435 Ok(mut out_file) => {
436 if let Err(e) = stream_file_to_writer(fs, &temp_path, &mut out_file) {
437 return Err(e.into());
439 }
440 out_file.sync_all()?;
441 #[allow(clippy::let_underscore_must_use)]
443 let _ = fs.remove_file(&temp_path);
444 #[allow(clippy::let_underscore_must_use)]
445 let _ = fs.remove_file(&recovery_meta_path);
446 Ok(())
447 }
448 Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
449 #[allow(clippy::let_underscore_must_use)]
452 let _ = fs.remove_file(&recovery_meta_path);
453 Err(make_sudo_error(temp_path, dest_path, original_metadata))
454 }
455 Err(e) => {
456 Err(e.into())
458 }
459 }
460}
461
462pub(super) fn write_data_inplace(
464 fs: &Arc<dyn FileSystem + Send + Sync>,
465 dest_path: &Path,
466 data: &[u8],
467 original_metadata: Option<FileMetadata>,
468) -> anyhow::Result<()> {
469 match fs.open_file_for_write(dest_path) {
470 Ok(mut out_file) => {
471 out_file.write_all(data)?;
472 out_file.sync_all()?;
473 Ok(())
474 }
475 Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
476 let (temp_path, mut temp_file) = create_temp_file(fs, dest_path)?;
478 temp_file.write_all(data)?;
479 temp_file.sync_all()?;
480 drop(temp_file);
481 Err(make_sudo_error(temp_path, dest_path, original_metadata))
482 }
483 Err(e) => Err(e.into()),
484 }
485}
486
487pub(super) fn stream_file_to_writer(
489 fs: &Arc<dyn FileSystem + Send + Sync>,
490 src_path: &Path,
491 out_file: &mut Box<dyn FileWriter>,
492) -> io::Result<()> {
493 const CHUNK_SIZE: usize = 1024 * 1024; let file_size = fs.metadata(src_path)?.size;
496 let mut offset = 0u64;
497
498 while offset < file_size {
499 let remaining = file_size - offset;
500 let chunk_len = std::cmp::min(remaining, CHUNK_SIZE as u64) as usize;
501 let chunk = fs.read_range(src_path, offset, chunk_len)?;
502 out_file.write_all(&chunk)?;
503 offset += chunk_len as u64;
504 }
505
506 Ok(())
507}
508
509pub(super) fn write_recipe_to_file(
511 fs: &Arc<dyn FileSystem + Send + Sync>,
512 out_file: &mut Box<dyn FileWriter>,
513 recipe: &WriteRecipe,
514) -> io::Result<()> {
515 for action in &recipe.actions {
516 match action {
517 RecipeAction::Copy { offset, len } => {
518 let src_path = recipe.src_path.as_ref().ok_or_else(|| {
520 io::Error::new(io::ErrorKind::InvalidData, "Copy action without source")
521 })?;
522 let data = fs.read_range(src_path, *offset, *len as usize)?;
523 out_file.write_all(&data)?;
524 }
525 RecipeAction::Insert { index } => {
526 out_file.write_all(&recipe.insert_data[*index])?;
527 }
528 }
529 }
530 Ok(())
531}
532
533pub(super) fn make_sudo_error(
535 temp_path: PathBuf,
536 dest_path: &Path,
537 original_metadata: Option<FileMetadata>,
538) -> anyhow::Error {
539 #[cfg(unix)]
540 let (uid, gid, mode) = if let Some(ref meta) = original_metadata {
541 (
542 meta.uid.unwrap_or(0),
543 meta.gid.unwrap_or(0),
544 meta.permissions
545 .as_ref()
546 .map(|p| p.mode() & 0o7777)
547 .unwrap_or(0),
548 )
549 } else {
550 (0, 0, 0)
551 };
552 #[cfg(not(unix))]
553 let (uid, gid, mode) = (0u32, 0u32, 0u32);
554
555 let _ = original_metadata; anyhow::anyhow!(SudoSaveRequired {
558 temp_path,
559 dest_path: dest_path.to_path_buf(),
560 uid,
561 gid,
562 mode,
563 })
564}