rawcopy_rs/
lib.rs

1// Copyright 2021 Colin Finck <colin@reactos.org>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3//
4//! `RawCopy` crate provides the capability to use "Volume Shadow Copy technology" for file copying in Rust.  
5//! Primarily aimed at replicating files that cannot be directly copied due to being in use.
6//!
7//! ```ignore
8//!    rawcopy_rs::rawcopy(file_path, save_path)?;
9//! ```
10//!
11
12mod sector_reader;
13use anyhow::{bail, Context, Result};
14
15
16use ntfs::indexes::NtfsFileNameIndex;
17use ntfs::structured_values::{
18    NtfsFileName, NtfsFileNamespace,
19};
20use ntfs::{Ntfs, NtfsFile, NtfsReadSeek};
21use sector_reader::SectorReader;
22
23use std::collections::VecDeque;
24use std::fs::{File, OpenOptions};
25
26use std::io::{BufReader, Read, Seek, Write};
27use std::path::{Component, PathBuf};
28
29use std::{path};
30
31struct CommandInfo<'n, T>
32where
33    T: Read + Seek,
34{
35    current_directory: Vec<NtfsFile<'n>>,
36    current_directory_string: String,
37    fs: T,
38    ntfs: &'n Ntfs,
39}
40///
41/// copy file from `file_path` to `save_path`
42/// 
43/// params: <p>`file_path` is the absolute path of the file must exist. </p>
44/// <p><pre>`save_path` is the directory where the copied file will be saved.  
45///             The directory must exist, and the file must not exist.  
46///             The file name will be the same as the name of the file being copied.  
47///             If it points to an NTFS filesystem image, then a suffix will be appended.</pre></p>
48/// 
49/// 
50pub fn rawcopy(file_path: &str, save_path: &str) -> Result<()> {
51    let path = path::PathBuf::from(file_path);
52
53    // let path = path.canonicalize().unwrap();
54    // path.components 左往右迭代
55    let mut components: VecDeque<_> = path
56        .components()
57        .filter_map(|comp| {
58            if Component::RootDir != comp {
59                Some(comp.as_os_str().to_str().unwrap())
60            } else {
61                None
62            }
63        })
64        .collect();
65
66    let prefix = components.pop_front().unwrap();
67    let file = components.pop_back().unwrap();
68
69    let prefix = format!(r"\\.\{prefix}");// wait https://github.com/dylni/normpath update tobe remove this line.
70
71    let f = File::open(prefix)?;
72    let sr = SectorReader::new(f, 4096)?;
73    let mut fs = BufReader::new(sr);
74    let mut ntfs = Ntfs::new(&mut fs)?;
75    ntfs.read_upcase_table(&mut fs)?;
76    let current_directory = vec![ntfs.root_directory(&mut fs)?];
77
78    let mut info = CommandInfo {
79        current_directory,
80        current_directory_string: String::new(),
81        fs,
82        ntfs: &ntfs,
83    };
84
85    for ele in components {
86        // println!("{}", ele);
87        cd(ele, &mut info)?;
88    }
89
90    get(file, save_path, &mut info)?;
91    Ok(())
92}
93fn best_file_name<T>(
94    info: &mut CommandInfo<T>,
95    file: &NtfsFile,
96    parent_record_number: u64,
97) -> Result<NtfsFileName>
98where
99    T: Read + Seek,
100{
101    // Try to find a long filename (Win32) first.
102    // If we don't find one, the file may only have a single short name (Win32AndDos).
103    // If we don't find one either, go with any namespace. It may still be a Dos or Posix name then.
104    let priority = [
105        Some(NtfsFileNamespace::Win32),
106        Some(NtfsFileNamespace::Win32AndDos),
107        None,
108    ];
109
110    for match_namespace in priority {
111        if let Some(file_name) =
112            file.name(&mut info.fs, match_namespace, Some(parent_record_number))
113        {
114            let file_name = file_name?;
115            return Ok(file_name);
116        }
117    }
118
119    bail!(
120        "Found no FileName attribute for File Record {:#x}",
121        file.file_record_number()
122    )
123}
124fn cd<T>(arg: &str, info: &mut CommandInfo<T>) -> Result<()>
125where
126    T: Read + Seek,
127{
128    if arg.is_empty() {
129        return Ok(());
130    }
131
132    if arg == ".." {
133        if info.current_directory_string.is_empty() {
134            return Ok(());
135        }
136
137        info.current_directory.pop();
138
139        let new_len = info.current_directory_string.rfind('\\').unwrap_or(0);
140        info.current_directory_string.truncate(new_len);
141    } else {
142        let index = info
143            .current_directory
144            .last()
145            .unwrap()
146            .directory_index(&mut info.fs)?;
147        let mut finder = index.finder();
148        let maybe_entry = NtfsFileNameIndex::find(&mut finder, info.ntfs, &mut info.fs, arg);
149
150        if maybe_entry.is_none() {
151            println!("Cannot find subdirectory \"{arg}\".");
152            return Ok(());
153        }
154
155        let entry = maybe_entry.unwrap()?;
156        let file_name = entry
157            .key()
158            .expect("key must exist for a found Index Entry")?;
159
160        if !file_name.is_directory() {
161            println!("\"{arg}\" is not a directory.");
162            return Ok(());
163        }
164
165        let file = entry.to_file(info.ntfs, &mut info.fs)?;
166        let file_name = best_file_name(
167            info,
168            &file,
169            info.current_directory.last().unwrap().file_record_number(),
170        )?;
171        if !info.current_directory_string.is_empty() {
172            info.current_directory_string += "\\";
173        }
174        info.current_directory_string += &file_name.name().to_string_lossy();
175
176        info.current_directory.push(file);
177    }
178
179    Ok(())
180}
181fn get<T>(file: &str, save_path: &str, info: &mut CommandInfo<T>) -> Result<()>
182where
183    T: Read + Seek,
184{
185    // Extract any specific $DATA stream name from the file.
186    let (file_name, data_stream_name) = match file.find(':') {
187        Some(mid) => (&file[..mid], &file[mid + 1..]),
188        None => (file, ""),
189    };
190
191    // Compose the output file name and try to create it.
192    // It must not yet exist, as we don't want to accidentally overwrite things.
193    let output_file_name = if data_stream_name.is_empty() {
194        file_name.to_string()
195    } else {
196        format!("{file_name}_{data_stream_name}")
197    };
198    let output_file_path = [save_path, &output_file_name].iter().collect::<PathBuf>();
199    let mut output_file = OpenOptions::new()
200        .write(true)
201        .create_new(true)
202        .open(&output_file_path)
203        .with_context(|| format!("Tried to open \"{output_file_name}\" for writing"))?;
204
205    // Open the desired file and find the $DATA attribute we are looking for.
206    let file = parse_file_arg(file_name, info)?;
207    let data_item = match file.data(&mut info.fs, data_stream_name) {
208        Some(data_item) => data_item,
209        None => {
210            println!("The file does not have a \"{data_stream_name}\" $DATA attribute.");
211            return Ok(());
212        }
213    };
214    let data_item = data_item?;
215    let data_attribute = data_item.to_attribute()?;
216    let mut data_value = data_attribute.value(&mut info.fs)?;
217
218    println!(
219        "Saving {} bytes of data in \"{}\"...",
220        data_value.len(),
221        output_file_name
222    );
223    let mut buf = [0u8; 4096];
224
225    loop {
226        let bytes_read = data_value.read(&mut info.fs, &mut buf)?;
227        if bytes_read == 0 {
228            break;
229        }
230
231        output_file.write_all(&buf[..bytes_read])?;
232    }
233    println!("Done! save to {}", &output_file_path.to_str().unwrap());
234    Ok(())
235}
236#[allow(clippy::from_str_radix_10)]
237fn parse_file_arg<'n, T>(arg: &str, info: &mut CommandInfo<'n, T>) -> Result<NtfsFile<'n>>
238where
239    T: Read + Seek,
240{
241    if arg.is_empty() {
242        bail!("Missing argument!");
243    }
244
245    if let Some(record_number_arg) = arg.strip_prefix('/') {
246        let record_number = match record_number_arg.strip_prefix("0x") {
247            Some(hex_record_number_arg) => u64::from_str_radix(hex_record_number_arg, 16),
248            None => u64::from_str_radix(record_number_arg, 10),
249        };
250
251        if let Ok(record_number) = record_number {
252            let file = info.ntfs.file(&mut info.fs, record_number)?;
253            Ok(file)
254        } else {
255            bail!(
256                "Cannot parse record number argument \"{}\"",
257                record_number_arg
258            )
259        }
260    } else {
261        let index = info
262            .current_directory
263            .last()
264            .unwrap()
265            .directory_index(&mut info.fs)?;
266        let mut finder = index.finder();
267
268        if let Some(entry) = NtfsFileNameIndex::find(&mut finder, info.ntfs, &mut info.fs, arg) {
269            let entry = entry?;
270            let file = entry.to_file(info.ntfs, &mut info.fs)?;
271            Ok(file)
272        } else {
273            bail!("No such file or directory \"{}\".", arg)
274        }
275    }
276}