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