From 25eafeadf08515703ada82336dd2afb4e7c7077c Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Mon, 17 Jun 2024 19:05:32 +0800 Subject: [PATCH 01/34] fix: rename and fetchdata (#3) * fix: rename and fetch_data progress report * fix: delete and rename --- examples/sftp/src/main.rs | 147 ++++++++++++-------------------------- src/filter/info.rs | 14 ++-- src/filter/proxy.rs | 6 +- 3 files changed, 57 insertions(+), 110 deletions(-) diff --git a/examples/sftp/src/main.rs b/examples/sftp/src/main.rs index b34d9fe..773e8a0 100644 --- a/examples/sftp/src/main.rs +++ b/examples/sftp/src/main.rs @@ -1,7 +1,7 @@ use std::{ env, ffi::OsStr, - fs::{self, File}, + fs::File, io::{self, BufWriter, Read, Seek, SeekFrom, Write}, net::TcpStream, os::windows::fs::OpenOptionsExt, @@ -18,14 +18,14 @@ use wincs::{ filter::{info, ticket, SyncFilter}, placeholder_file::{Metadata, PlaceholderFile}, request::Request, - CloudErrorKind, PopulationType, Registration, SecurityId, SyncRootIdBuilder, + CloudErrorKind, HydrationType, PopulationType, Registration, SecurityId, SyncRootIdBuilder, }; // max should be 65536, this is done both in term-scp and sshfs because it's the // max packet size for a tcp connection const DOWNLOAD_CHUNK_SIZE_BYTES: usize = 4096; // doesn't have to be 4KiB aligned -const UPLOAD_CHUNK_SIZE_BYTES: usize = 4096; +// const UPLOAD_CHUNK_SIZE_BYTES: usize = 4096; const PROVIDER_NAME: &str = "wincs"; const DISPLAY_NAME: &str = "Sftp"; @@ -59,7 +59,7 @@ fn main() { let u16_display_name = U16String::from_str(DISPLAY_NAME); Registration::from_sync_root_id(&sync_root_id) .display_name(&u16_display_name) - .hydration_type(wincs::HydrationType::Full) + .hydration_type(HydrationType::Full) .population_type(PopulationType::Full) .icon( U16String::from_str("%SystemRoot%\\system32\\charmap.exe"), @@ -119,52 +119,10 @@ pub struct Filter { } impl Filter { - pub fn create_file(&self, src: &Path, dest: &Path) -> Result<(), SftpError> { - let mut client_file = File::open(src)?; - // TODO: This will overwrite the file if it exists on the server - let mut server_file = self.sftp.create(dest)?; - - let mut buffer = [0; UPLOAD_CHUNK_SIZE_BYTES]; - let mut bytes_written = 0; - - // TODO: I could do the little offset trick and moving the old bytes to the - // beginning of the buffer, I just don't know if it's worth it - loop { - client_file.seek(SeekFrom::Start(bytes_written))?; - match client_file.read(&mut buffer) { - Ok(0) => break, - Ok(bytes_read) => { - bytes_written += server_file.write(&buffer[0..bytes_read])? as u64; - } - Err(err) if err.kind() == io::ErrorKind::Interrupted => {} - Err(err) => return Err(SftpError::Io(err)), - } - } - - Ok(()) - } - - // TODO: src is full, dest is relative - pub fn create_dir_all(&self, src: &Path, dest: &Path) -> Result<(), SftpError> { - // TODO: what does the "o" mean in 0o775 - self.sftp.mkdir(dest, 0o775)?; - - for entry in fs::read_dir(src)? { - let src = entry?.path(); - let dest = dest.join(src.file_name().unwrap()); - match src.is_dir() { - true => self.create_dir_all(&src, &dest)?, - false => self.create_file(&src, &dest)?, - } - } - - Ok(()) - } - - pub fn remove_dir_all(&self, dest: &Path) -> Result<(), ssh2::Error> { + pub fn remove_remote_dir_all(&self, dest: &Path) -> Result<(), ssh2::Error> { for entry in self.sftp.readdir(dest)? { - match entry.0.is_dir() { - true => self.remove_dir_all(&entry.0)?, + match entry.1.is_dir() { + true => self.remove_remote_dir_all(&entry.0)?, false => self.sftp.unlink(&entry.0)?, } } @@ -173,28 +131,27 @@ impl Filter { } } -// TODO: handle unwraps -// TODO: everything is just forwarded to external functions... This should be -// changed in the wrapper api impl SyncFilter for Filter { - // TODO: handle unwraps fn fetch_data(&self, request: Request, ticket: ticket::FetchData, info: info::FetchData) { - println!("fetch_data {:?}", request.file_blob()); - // TODO: handle unwrap let path = Path::new(unsafe { OsStr::from_encoded_bytes_unchecked(request.file_blob()) }); let range = info.required_file_range(); let end = range.end; let mut position = range.start; - // TODO: allow callback to return Result in SyncFilter + println!( + "fetch_data {:?} {:?} {}", + path, + range, + info.interrupted_hydration() + ); + let res = || -> Result<(), _> { let mut server_file = self .sftp .open(path) .map_err(|_| CloudErrorKind::InvalidRequest)?; let mut client_file = BufWriter::with_capacity(4096, request.placeholder()); - server_file .seek(SeekFrom::Start(position)) .map_err(|_| CloudErrorKind::InvalidRequest)?; @@ -208,12 +165,10 @@ impl SyncFilter for Filter { // into segments done on separate threads // transfer the data in chunks loop { - client_file.get_ref().set_progress(end, position).unwrap(); - // TODO: read directly to the BufWriters buffer // TODO: ignore if the error was just interrupted let bytes_read = server_file - .read(&mut buffer[0..DOWNLOAD_CHUNK_SIZE_BYTES]) + .read(&mut buffer) .map_err(|_| CloudErrorKind::InvalidRequest)?; let bytes_written = client_file .write(&buffer[0..bytes_read]) @@ -223,6 +178,8 @@ impl SyncFilter for Filter { if position >= end { break; } + + client_file.get_ref().set_progress(end, position).unwrap(); } client_file @@ -241,14 +198,13 @@ impl SyncFilter for Filter { println!("deleted"); } - // TODO: I probably also have to delete the file from the disk fn delete(&self, request: Request, ticket: ticket::Delete, info: info::Delete) { println!("delete {:?}", request.path()); let path = Path::new(unsafe { OsStr::from_encoded_bytes_unchecked(request.file_blob()) }); let res = || -> Result<(), _> { match info.is_directory() { true => self - .remove_dir_all(path) + .remove_remote_dir_all(path) .map_err(|_| CloudErrorKind::InvalidRequest)?, false => self .sftp @@ -264,44 +220,35 @@ impl SyncFilter for Filter { } } - // TODO: Do I have to move the file and set the file progress? or does the OS - // handle that? (I think I do) fn rename(&self, request: Request, ticket: ticket::Rename, info: info::Rename) { let res = || -> Result<(), _> { - match info.target_in_scope() { - true => { - // TODO: path should auto include the drive letter - let src = request.path(); - // TODO: should be relative - let dest = info.target_path(); - - match info.source_in_scope() { - // TODO: use fs::copy or fs::rename, whatever it is to move the local files, - // then use ConvertToPlaceholder. I'm not sure if I have to do this recursively - // for each file or only the top-level folder TODO: which - // rename flags do I use? how do I know if I should be overwriting? - true => self - .sftp - .rename(&src, &dest, None) - .map_err(|_| CloudErrorKind::InvalidRequest)?, - false => match info.is_directory() { - true => self - .create_dir_all(&src, &dest) - .map_err(|_| CloudErrorKind::InvalidRequest)?, - false => self - .create_file(&src, &dest) - .map_err(|_| CloudErrorKind::InvalidRequest)?, - }, - } + let src = request.path(); + let dest = info.target_path(); + let base = get_client_path(); + + println!( + "rename {} to {}, source in scope: {}, target in scope: {}", + src.display(), + dest.display(), + info.source_in_scope(), + info.target_in_scope() + ); + + match (info.source_in_scope(), info.target_in_scope()) { + (true, true) => { + self.sftp + .rename( + &src.strip_prefix(&base).unwrap(), + &dest.strip_prefix(&base).unwrap(), + None, + ) + .map_err(|_| CloudErrorKind::InvalidRequest)?; } - // TODO: do I need to delete it locally? - false => self - .sftp - .unlink(Path::new(unsafe { - OsStr::from_encoded_bytes_unchecked(request.file_blob()) - })) - .map_err(|_| CloudErrorKind::InvalidRequest)?, + (true, false) => {} + (false, true) => Err(CloudErrorKind::NotSupported)?, // TODO + (false, false) => Err(CloudErrorKind::InvalidRequest)?, } + ticket.pass().unwrap(); Ok(()) }(); @@ -327,7 +274,7 @@ impl SyncFilter for Filter { let parent = absolute.strip_prefix(&client_path).unwrap(); let dirs = self.sftp.readdir(parent).unwrap(); - let placeholders = dirs + let mut placeholders = dirs .into_iter() .filter(|(path, _)| !Path::new(&client_path).join(path).exists()) .map(|(path, stat)| { @@ -348,13 +295,13 @@ impl SyncFilter for Filter { .last_write_time(stat.mtime.unwrap_or_default()) .change_time(stat.mtime.unwrap_or_default()), ) + .mark_sync() .overwrite() - // .mark_sync() // need this? .blob(path.into_os_string().into_encoded_bytes()) }) .collect::>(); - ticket.pass_with_placeholder(placeholders).unwrap(); + ticket.pass_with_placeholder(&mut placeholders).unwrap(); } fn closed(&self, request: Request, info: info::Closed) { @@ -401,8 +348,6 @@ impl SyncFilter for Filter { fn renamed(&self, _request: Request, _info: info::Renamed) { println!("renamed"); } - - // TODO: acknowledgement callbacks } #[derive(Error, Debug)] diff --git a/src/filter/info.rs b/src/filter/info.rs index ffcc50a..c9f6740 100644 --- a/src/filter/info.rs +++ b/src/filter/info.rs @@ -1,4 +1,4 @@ -use std::{fmt::Debug, ops::Range, path::PathBuf}; +use std::{ffi::OsString, fmt::Debug, ops::Range, path::PathBuf}; use widestring::U16CStr; use windows::Win32::Storage::CloudFilters::{ @@ -266,10 +266,10 @@ pub struct Deleted(pub(crate) CF_CALLBACK_PARAMETERS_0_4); /// Information for the [SyncFilter::rename][crate::SyncFilter::rename] callback. #[derive(Debug)] -pub struct Rename(pub(crate) CF_CALLBACK_PARAMETERS_0_10); +pub struct Rename(pub(crate) CF_CALLBACK_PARAMETERS_0_10, pub(crate) OsString); impl Rename { - /// Whether or not the placeholder being deleted is a directory. + /// Whether or not the placeholder being renamed is a directory. pub fn is_directory(&self) -> bool { (self.0.Flags & CloudFilters::CF_CALLBACK_RENAME_FLAG_IS_DIRECTORY).0 != 0 } @@ -286,11 +286,9 @@ impl Rename { /// The full path the placeholder is being moved to. pub fn target_path(&self) -> PathBuf { - unsafe { - U16CStr::from_ptr_str(self.0.TargetPath.0) - .to_os_string() - .into() - } + let mut path = PathBuf::from(&self.1); + path.push(unsafe { U16CStr::from_ptr_str(self.0.TargetPath.0) }.to_os_string()); + path } } diff --git a/src/filter/proxy.rs b/src/filter/proxy.rs index baf6a2f..49b19cb 100644 --- a/src/filter/proxy.rs +++ b/src/filter/proxy.rs @@ -232,8 +232,12 @@ pub unsafe extern "system" fn notify_rename( if let Some(filter) = filter_from_info::(info) { let request = Request::new(*info); let ticket = ticket::Rename::new(request.connection_key(), request.transfer_key()); + let info = info::Rename( + (*params).Anonymous.Rename, + request.volume_letter().to_os_string(), + ); - filter.rename(request, ticket, info::Rename((*params).Anonymous.Rename)); + filter.rename(request, ticket, info); } } From 74f0fd1771a7eb67793bb4bda14767d8ee12f363 Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Wed, 19 Jun 2024 19:19:43 +0800 Subject: [PATCH 02/34] refactor: tickets --- examples/sftp/src/main.rs | 36 ++++++++--------- src/command/commands.rs | 29 +++++++------ src/filter/ticket.rs | 85 ++++++++++++++++++++++++++++++++++----- src/lib.rs | 7 +++- src/placeholder.rs | 4 +- src/utility.rs | 14 ++++++- 6 files changed, 127 insertions(+), 48 deletions(-) diff --git a/examples/sftp/src/main.rs b/examples/sftp/src/main.rs index 773e8a0..d76e17a 100644 --- a/examples/sftp/src/main.rs +++ b/examples/sftp/src/main.rs @@ -2,7 +2,7 @@ use std::{ env, ffi::OsStr, fs::File, - io::{self, BufWriter, Read, Seek, SeekFrom, Write}, + io::{self, Read, Seek, SeekFrom}, net::TcpStream, os::windows::fs::OpenOptionsExt, path::Path, @@ -19,11 +19,12 @@ use wincs::{ placeholder_file::{Metadata, PlaceholderFile}, request::Request, CloudErrorKind, HydrationType, PopulationType, Registration, SecurityId, SyncRootIdBuilder, + WriteAt, }; // max should be 65536, this is done both in term-scp and sshfs because it's the // max packet size for a tcp connection -const DOWNLOAD_CHUNK_SIZE_BYTES: usize = 4096; +const DOWNLOAD_CHUNK_SIZE_BYTES: usize = 65536; // doesn't have to be 4KiB aligned // const UPLOAD_CHUNK_SIZE_BYTES: usize = 4096; @@ -151,41 +152,36 @@ impl SyncFilter for Filter { .sftp .open(path) .map_err(|_| CloudErrorKind::InvalidRequest)?; - let mut client_file = BufWriter::with_capacity(4096, request.placeholder()); server_file .seek(SeekFrom::Start(position)) .map_err(|_| CloudErrorKind::InvalidRequest)?; - client_file - .seek(SeekFrom::Start(position)) - .map_err(|_| CloudErrorKind::InvalidRequest)?; let mut buffer = [0; DOWNLOAD_CHUNK_SIZE_BYTES]; - // TODO: move to a func and remove unwraps & allow to split up the entire read - // into segments done on separate threads - // transfer the data in chunks loop { - // TODO: read directly to the BufWriters buffer - // TODO: ignore if the error was just interrupted - let bytes_read = server_file + let mut bytes_read = server_file .read(&mut buffer) .map_err(|_| CloudErrorKind::InvalidRequest)?; - let bytes_written = client_file - .write(&buffer[0..bytes_read]) + + if bytes_read % 4096 != 0 && position + (bytes_read as u64) < end { + let unaligned = bytes_read % 4096; + bytes_read = bytes_read - unaligned; + server_file + .seek(SeekFrom::Current(-(unaligned as i64))) + .unwrap(); + } + ticket + .write_at(&buffer[0..bytes_read], position) .map_err(|_| CloudErrorKind::InvalidRequest)?; - position += bytes_written as u64; + position += bytes_read as u64; if position >= end { break; } - client_file.get_ref().set_progress(end, position).unwrap(); + ticket.report_progress(end, position).unwrap(); } - client_file - .flush() - .map_err(|_| CloudErrorKind::InvalidRequest)?; - Ok(()) }(); diff --git a/src/command/commands.rs b/src/command/commands.rs index 1d1552a..0cac37d 100644 --- a/src/command/commands.rs +++ b/src/command/commands.rs @@ -116,7 +116,7 @@ pub struct Update<'a> { /// Optional metadata to update. pub metadata: Option, /// Optional file blob to update. - pub blob: Option<&'a [u8]>, + pub blob: &'a [u8], } impl Command for Update<'_> { @@ -135,13 +135,15 @@ impl Command for Update<'_> { } else { CloudFilters::CF_OPERATION_RESTART_HYDRATION_FLAG_NONE }, - FsMetadata: self.metadata.map_or(ptr::null_mut(), |mut metadata| { - &mut metadata as *mut _ as *mut _ - }), - FileIdentity: self - .blob - .map_or(ptr::null_mut(), |blob| blob.as_ptr() as *mut _), - FileIdentityLength: self.blob.map_or(0, |blob| blob.len() as u32), + FsMetadata: self + .metadata + .as_ref() + .map_or(ptr::null(), |metadata| &metadata.0 as *const _), + FileIdentity: match self.blob.is_empty() { + true => ptr::null(), + false => self.blob.as_ptr() as *const _, + }, + FileIdentityLength: self.blob.len() as _, }, } } @@ -279,7 +281,7 @@ impl Fallible for Validate { #[derive(Debug)] pub struct Dehydrate<'a> { /// Optional file blob to update. - pub blob: Option<&'a [u8]>, + pub blob: &'a [u8], } impl Command for Dehydrate<'_> { @@ -295,10 +297,11 @@ impl Command for Dehydrate<'_> { AckDehydrate: CF_OPERATION_PARAMETERS_0_1 { Flags: CloudFilters::CF_OPERATION_ACK_DEHYDRATE_FLAG_NONE, CompletionStatus: Foundation::STATUS_SUCCESS, - FileIdentity: self - .blob - .map_or(ptr::null(), |blob| blob.as_ptr() as *const _), - FileIdentityLength: self.blob.map_or(0, |blob| blob.len() as u32), + FileIdentity: match self.blob.is_empty() { + true => ptr::null(), + false => self.blob.as_ptr() as *const _, + }, + FileIdentityLength: self.blob.len() as u32, }, } } diff --git a/src/filter/ticket.rs b/src/filter/ticket.rs index febe9d7..5514757 100644 --- a/src/filter/ticket.rs +++ b/src/filter/ticket.rs @@ -1,12 +1,15 @@ use std::ops::Range; -use windows::core; +use windows::{ + core, + Win32::Storage::CloudFilters::{CfReportProviderProgress, CF_CONNECTION_KEY}, +}; use crate::{ command::{self, Command, Fallible}, error::CloudErrorKind, request::{RawConnectionKey, RawTransferKey}, - PlaceholderFile, Usn, + sealed, utility, PlaceholderFile, Usn, }; /// A ticket for the [SyncFilter::fetch_data][crate::SyncFilter::fetch_data] callback. @@ -18,7 +21,7 @@ pub struct FetchData { impl FetchData { /// Create a new [FetchData][crate::ticket::FetchData]. - pub fn new(connection_key: RawConnectionKey, transfer_key: RawTransferKey) -> Self { + pub(crate) fn new(connection_key: RawConnectionKey, transfer_key: RawTransferKey) -> Self { Self { connection_key, transfer_key, @@ -29,8 +32,52 @@ impl FetchData { pub fn fail(&self, error_kind: CloudErrorKind) -> core::Result<()> { command::Write::fail(self.connection_key, self.transfer_key, error_kind) } + + pub fn report_progress(&self, total: u64, completed: u64) -> core::Result<()> { + unsafe { + CfReportProviderProgress( + CF_CONNECTION_KEY(self.connection_key), + self.transfer_key, + total as i64, + completed as i64, + ) + }?; + + Ok(()) + } +} + +impl utility::ReadAt for FetchData { + /// Read data at an offset from a placeholder file. + /// + /// This method is equivalent to calling `CfExecute` with `CF_OPERATION_TYPE_RETRIEVE_DATA`. + fn read_at(&self, buf: &mut [u8], offset: u64) -> core::Result { + command::Read { + buffer: buf, + position: offset, + } + .execute(self.connection_key, self.transfer_key) + } +} + +impl utility::WriteAt for FetchData { + /// Write data at an offset to a placeholder file. + /// + /// The buffer passed must be 4KiB in length or end on the logical file size. Unfortunately, + /// this is a restriction of the operating system. + /// + /// This method is equivalent to calling `CfExecute` with `CF_OPERATION_TYPE_TRANSFER_DATA`. + fn write_at(&self, buf: &[u8], offset: u64) -> core::Result<()> { + command::Write { + buffer: buf, + position: offset, + } + .execute(self.connection_key, self.transfer_key) + } } +impl sealed::Sealed for FetchData {} + /// A ticket for the [SyncFilter::validate_data][crate::SyncFilter::validate_data] callback. #[derive(Debug)] pub struct ValidateData { @@ -40,7 +87,7 @@ pub struct ValidateData { impl ValidateData { /// Create a new [ValidateData][crate::ticket::ValidateData]. - pub fn new(connection_key: RawConnectionKey, transfer_key: RawTransferKey) -> Self { + pub(crate) fn new(connection_key: RawConnectionKey, transfer_key: RawTransferKey) -> Self { Self { connection_key, transfer_key, @@ -61,6 +108,24 @@ impl ValidateData { } } +impl utility::ReadAt for ValidateData { + /// Read data at an offset from a placeholder file. + /// + /// This method is equivalent to calling `CfExecute` with `CF_OPERATION_TYPE_RETRIEVE_DATA`. + /// + /// The bytes returned will ALWAYS be the length of the buffer passed in. The operating + /// system provides these guarantees. + fn read_at(&self, buf: &mut [u8], offset: u64) -> core::Result { + command::Read { + buffer: buf, + position: offset, + } + .execute(self.connection_key, self.transfer_key) + } +} + +impl sealed::Sealed for ValidateData {} + /// A ticket for the [SyncFilter::fetch_placeholders][crate::SyncFilter::fetch_placeholders] callback. #[derive(Debug)] pub struct FetchPlaceholders { @@ -70,7 +135,7 @@ pub struct FetchPlaceholders { impl FetchPlaceholders { /// Create a new [FetchPlaceholders][crate::ticket::FetchPlaceholders]. - pub fn new(connection_key: RawConnectionKey, transfer_key: RawTransferKey) -> Self { + pub(crate) fn new(connection_key: RawConnectionKey, transfer_key: RawTransferKey) -> Self { Self { connection_key, transfer_key, @@ -107,7 +172,7 @@ pub struct Dehydrate { impl Dehydrate { /// Create a new [Dehydrate][crate::ticket::Dehydrate]. - pub fn new(connection_key: RawConnectionKey, transfer_key: RawTransferKey) -> Self { + pub(crate) fn new(connection_key: RawConnectionKey, transfer_key: RawTransferKey) -> Self { Self { connection_key, transfer_key, @@ -116,12 +181,12 @@ impl Dehydrate { /// Confirms dehydration of the file. pub fn pass(&self) -> core::Result<()> { - command::Dehydrate { blob: None }.execute(self.connection_key, self.transfer_key) + command::Dehydrate { blob: &[] }.execute(self.connection_key, self.transfer_key) } /// Confirms dehydration of the file and updates its file blob. pub fn pass_with_blob(&self, blob: &[u8]) -> core::Result<()> { - command::Dehydrate { blob: Some(blob) }.execute(self.connection_key, self.transfer_key) + command::Dehydrate { blob }.execute(self.connection_key, self.transfer_key) } /// Fail the callback with the specified error. @@ -139,7 +204,7 @@ pub struct Delete { impl Delete { /// Create a new [Delete][crate::ticket::Delete]. - pub fn new(connection_key: RawConnectionKey, transfer_key: RawTransferKey) -> Self { + pub(crate) fn new(connection_key: RawConnectionKey, transfer_key: RawTransferKey) -> Self { Self { connection_key, transfer_key, @@ -166,7 +231,7 @@ pub struct Rename { impl Rename { /// Create a new [Rename][crate::ticket::Rename]. - pub fn new(connection_key: RawConnectionKey, transfer_key: RawTransferKey) -> Self { + pub(crate) fn new(connection_key: RawConnectionKey, transfer_key: RawTransferKey) -> Self { Self { connection_key, transfer_key, diff --git a/src/lib.rs b/src/lib.rs index 8effa2f..efccf03 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,7 +13,7 @@ pub mod placeholder_file; pub mod request; pub mod root; pub mod usn; -mod utility; +pub mod utility; pub use error::CloudErrorKind; pub use filter::{info, ticket, SyncFilter}; @@ -26,3 +26,8 @@ pub use root::{ SyncRootIdBuilder, }; pub use usn::Usn; +pub use utility::{ReadAt, WriteAt}; + +mod sealed { + pub trait Sealed {} +} diff --git a/src/placeholder.rs b/src/placeholder.rs index 62da5d0..67357bc 100644 --- a/src/placeholder.rs +++ b/src/placeholder.rs @@ -298,7 +298,7 @@ impl<'a> UpdateOptions<'a> { CloudFilters::CF_PLACEHOLDER_MAX_FILE_IDENTITY_LENGTH, blob.len() ); - self.0.blob = Some(blob); + self.0.blob = blob; self } } @@ -308,7 +308,7 @@ impl<'a> Default for UpdateOptions<'a> { Self(Update { mark_sync: false, metadata: None, - blob: None, + blob: &[], }) } } diff --git a/src/utility.rs b/src/utility.rs index a1f4907..eb14958 100644 --- a/src/utility.rs +++ b/src/utility.rs @@ -1,8 +1,10 @@ -use windows::core::HSTRING; +use windows::core::{self, HSTRING}; + +use crate::sealed; // TODO: add something to convert an Option to a *const T and *mut T -pub trait ToHString +pub(crate) trait ToHString where Self: AsRef<[u16]>, { @@ -13,3 +15,11 @@ where } impl> ToHString for T {} + +pub trait ReadAt: sealed::Sealed { + fn read_at(&self, buf: &mut [u8], offset: u64) -> core::Result; +} + +pub trait WriteAt: sealed::Sealed { + fn write_at(&self, buf: &[u8], offset: u64) -> core::Result<()>; +} From 87e6b58e71058dc06e95f5bc5d75e97935253986 Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Sun, 23 Jun 2024 17:20:07 +0800 Subject: [PATCH 03/34] refactor: Placeholder --- examples/sftp/src/main.rs | 49 +-- src/ext/file.rs | 442 +-------------------- src/ext/mod.rs | 5 +- src/filter/ticket.rs | 15 +- src/lib.rs | 2 +- src/placeholder.rs | 799 ++++++++++++++++++++++++++------------ src/placeholder_file.rs | 10 +- src/request.rs | 37 +- 8 files changed, 628 insertions(+), 731 deletions(-) diff --git a/examples/sftp/src/main.rs b/examples/sftp/src/main.rs index d76e17a..838e255 100644 --- a/examples/sftp/src/main.rs +++ b/examples/sftp/src/main.rs @@ -4,7 +4,6 @@ use std::{ fs::File, io::{self, Read, Seek, SeekFrom}, net::TcpStream, - os::windows::fs::OpenOptionsExt, path::Path, sync::mpsc, }; @@ -14,18 +13,17 @@ use ssh2::{Session, Sftp}; use thiserror::Error; use widestring::{u16str, U16String}; use wincs::{ - ext::{ConvertOptions, FileExt}, filter::{info, ticket, SyncFilter}, + placeholder::ConvertOptions, placeholder_file::{Metadata, PlaceholderFile}, request::Request, - CloudErrorKind, HydrationType, PopulationType, Registration, SecurityId, SyncRootIdBuilder, - WriteAt, + CloudErrorKind, HydrationType, Placeholder, PopulationType, Registration, SecurityId, + SyncRootIdBuilder, WriteAt, }; // max should be 65536, this is done both in term-scp and sshfs because it's the // max packet size for a tcp connection const DOWNLOAD_CHUNK_SIZE_BYTES: usize = 65536; -// doesn't have to be 4KiB aligned // const UPLOAD_CHUNK_SIZE_BYTES: usize = 4096; const PROVIDER_NAME: &str = "wincs"; @@ -72,7 +70,7 @@ fn main() { .unwrap(); } - convert_to_placeholder(Path::new(&client_path)); + mark_in_sync(Path::new(&client_path), &sftp); let connection = wincs::Session::new() .connect(&client_path, Filter { sftp }) @@ -88,29 +86,36 @@ fn get_client_path() -> String { env::var("CLIENT_PATH").unwrap() } -fn convert_to_placeholder(path: &Path) { +fn mark_in_sync(path: &Path, sftp: &Sftp) { + let base = get_client_path(); for entry in path.read_dir().unwrap() { let entry = entry.unwrap(); - let is_dir = entry.path().is_dir(); + let remote_path = entry.path().strip_prefix(&base).unwrap().to_owned(); - let mut open_options = File::options(); - open_options.read(true); - if is_dir { - // FILE_FLAG_BACKUP_SEMANTICS, needed to obtain handle to directory - open_options.custom_flags(0x02000000); + let Ok(meta) = sftp.stat(&remote_path) else { + continue; + }; + if meta.is_dir() != entry.file_type().unwrap().is_dir() { + continue; } - let convert_options = if is_dir { - ConvertOptions::default().has_children() - } else { - ConvertOptions::default() + let mut options = ConvertOptions::default() + .mark_in_sync() + .blob(remote_path.clone().into_os_string().into_encoded_bytes()); + let mut placeholder = match meta.is_dir() { + true => { + options = options.has_children(); + Placeholder::open(entry.path()).unwrap() + } + false => File::open(entry.path()).unwrap().into(), }; - let file = open_options.open(entry.path()).unwrap(); - file.to_placeholder(convert_options).unwrap(); + _ = placeholder + .convert_to_placeholder(options, None) + .inspect_err(|e| println!("convert_to_placeholder {:?}", e)); - if is_dir { - convert_to_placeholder(&entry.path()); + if meta.is_dir() { + mark_in_sync(&entry.path(), sftp); } } } @@ -291,7 +296,7 @@ impl SyncFilter for Filter { .last_write_time(stat.mtime.unwrap_or_default()) .change_time(stat.mtime.unwrap_or_default()), ) - .mark_sync() + .mark_in_sync() .overwrite() .blob(path.into_os_string().into_encoded_bytes()) }) diff --git a/src/ext/file.rs b/src/ext/file.rs index fd85a7f..c35f52e 100644 --- a/src/ext/file.rs +++ b/src/ext/file.rs @@ -1,7 +1,7 @@ use std::{ fs::File, mem::{self, MaybeUninit}, - ops::{Bound, Range, RangeBounds}, + ops::{Bound, RangeBounds}, os::windows::{io::AsRawHandle, prelude::RawHandle}, ptr, }; @@ -13,14 +13,10 @@ use windows::{ Foundation::HANDLE, Storage::{ CloudFilters::{ - self, CfConvertToPlaceholder, CfDehydratePlaceholder, CfGetPlaceholderInfo, - CfGetPlaceholderRangeInfo, CfGetPlaceholderStateFromFileInfo, - CfGetSyncRootInfoByHandle, CfHydratePlaceholder, CfRevertPlaceholder, - CfSetInSyncState, CfSetPinState, CfUpdatePlaceholder, CF_CONVERT_FLAGS, - CF_FILE_RANGE, CF_PIN_STATE, CF_PLACEHOLDER_RANGE_INFO_CLASS, - CF_PLACEHOLDER_STANDARD_INFO, CF_PLACEHOLDER_STATE, CF_SET_PIN_FLAGS, - CF_SYNC_PROVIDER_STATUS, CF_SYNC_ROOT_INFO_STANDARD, CF_SYNC_ROOT_STANDARD_INFO, - CF_UPDATE_FLAGS, + self, CfDehydratePlaceholder, CfGetPlaceholderRangeInfo, + CfGetPlaceholderStateFromFileInfo, CfGetSyncRootInfoByHandle, CfHydratePlaceholder, + CfSetInSyncState, CF_PLACEHOLDER_RANGE_INFO_CLASS, CF_PLACEHOLDER_STATE, + CF_SYNC_PROVIDER_STATUS, CF_SYNC_ROOT_STANDARD_INFO, }, FileSystem::{self, GetFileInformationByHandleEx, FILE_ATTRIBUTE_TAG_INFO}, }, @@ -28,100 +24,12 @@ use windows::{ }; use crate::{ - placeholder_file::Metadata, root::{HydrationPolicy, HydrationType, PopulationType, SupportedAttributes}, usn::Usn, }; /// An API extension to [File][std::fs::File]. pub trait FileExt: AsRawHandle { - /// Converts a file to a placeholder file, returning the resulting USN. - /// - /// Restrictions: - /// * The file or directory must be the sync root directory itself, or a descendant directory. - /// * [CloudErrorKind::NotUnderSyncRoot][crate::CloudErrorKind::NotUnderSyncRoot] - /// * If [ConvertOptions::dehydrate][ConvertOptions::dehydrate] is selected, the sync root must - /// not be registered with [HydrationType::AlwaysFull][crate::HydrationType::AlwaysFull]. - /// * [CloudErrorKind::NotSupported][crate::CloudErrorKind::NotSupported] - /// * If [ConvertOptions::dehydrate][ConvertOptions::dehydrate] is selected, the placeholder - /// must not be pinned. - /// * [CloudErrorKind::Pinned][crate::CloudErrorKind::Pinned] - /// * The handle must have write access. - /// * [CloudErrorKind::AccessDenied][crate::CloudErrorKind::AccessDenied] - /// - /// [Read more - /// here](https://docs.microsoft.com/en-us/windows/win32/api/cfapi/nf-cfapi-cfconverttoplaceholder#remarks] - // TODO: the 4th remark on the link doesn't make sense? Seems to be copied and pasted from - // `CfUpdatePlaceholder`. - fn to_placeholder(&self, options: ConvertOptions) -> core::Result { - let mut usn = MaybeUninit::::uninit(); - unsafe { - CfConvertToPlaceholder( - HANDLE(self.as_raw_handle() as isize), - options - .blob - .map_or(ptr::null(), |blob| blob.as_ptr() as *const _), - options.blob.map_or(0, |blob| blob.len() as u32), - options.flags, - usn.as_mut_ptr(), - ptr::null_mut(), - ) - .map(|_| usn.assume_init() as Usn) - } - } - - /// Converts a placeholder file to a normal file. - /// - /// Restrictions: - /// * If the file is not already hydrated, it will implicitly call - /// [SyncFilter::fetch_data][crate::SyncFilter::fetch_data]. If the file can not be hydrated, - /// the conversion will fail. - /// The handle must have write access. - fn to_file(&self) -> core::Result<()> { - unsafe { - CfRevertPlaceholder( - HANDLE(self.as_raw_handle() as isize), - CloudFilters::CF_REVERT_FLAG_NONE, - ptr::null_mut(), - ) - } - } - - /// Updates various characteristics of a placeholder. - /// - /// Restrictions: - /// * The file or directory must be the sync root directory itself, or a descendant directory. - /// * [CloudErrorKind::NotUnderSyncRoot][crate::CloudErrorKind::NotUnderSyncRoot] - /// * If [UpdateOptions::dehydrate][UpdateOptions::dehydrate] is selected, the sync root must - /// not be registered with [HydrationType::AlwaysFull][crate::HydrationType::AlwaysFull]. - /// * [CloudErrorKind::NotSupported][crate::CloudErrorKind::NotSupported] - /// * If [UpdateOptions::dehydrate][UpdateOptions::dehydrate] is selected, the placeholder - /// must not be pinned. - /// * [CloudErrorKind::Pinned][crate::CloudErrorKind::Pinned] - /// * If [UpdateOptions::dehydrate][UpdateOptions::dehydrate] is selected, the placeholder - /// must be in sync. - /// * [CloudErrorKind::NotInSync][crate::CloudErrorKind::NotInSync] - /// * The handle must have write access. - /// * [CloudErrorKind::AccessDenied][crate::CloudErrorKind::AccessDenied] - // TODO: this could be split into multiple functions to make common patterns easier - fn update(&self, usn: Usn, mut options: UpdateOptions) -> core::Result { - let mut usn = usn as i64; - unsafe { - CfUpdatePlaceholder( - HANDLE(self.as_raw_handle() as isize), - options.metadata.map_or(ptr::null(), |x| &x.0 as *const _), - options.blob.map_or(ptr::null(), |x| x.as_ptr() as *const _), - options.blob.map_or(0, |x| x.len() as u32), - options.dehydrate_range.as_mut_ptr(), - options.dehydrate_range.len() as u32, - options.flags, - &mut usn as *mut _, - ptr::null_mut(), - ) - .map(|_| usn as Usn) - } - } - /// Hydrates a placeholder file. // TODO: doc restrictions. I believe the remarks are wrong in that this call requires both read // and write access? https://docs.microsoft.com/en-us/windows/win32/api/cfapi/nf-cfapi-cfhydrateplaceholder#remarks @@ -174,36 +82,6 @@ pub trait FileExt: AsRawHandle { .map(|_| length) } - /// Gets various characteristics of a placeholder. - fn placeholder_info(&self) -> core::Result { - // TODO: same as below except finds the size after 2 calls of CfGetPlaceholderInfo - todo!() - } - - /// Gets various characteristics of a placeholder using the passed blob size. - fn placeholder_info_unchecked(&self, blob_size: usize) -> core::Result { - let mut data = vec![0; mem::size_of::() + blob_size]; - - unsafe { - CfGetPlaceholderInfo( - HANDLE(self.as_raw_handle() as isize), - CloudFilters::CF_PLACEHOLDER_INFO_STANDARD, - data.as_mut_ptr() as *mut _, - data.len() as u32, - ptr::null_mut(), - )?; - } - - Ok(PlaceholderInfo { - info: &unsafe { - data[..=mem::size_of::()] - .align_to::() - } - .1[0] as *const _, - data, - }) - } - /// Gets the current state of the placeholder. // TODO: test to ensure this works. I feel like returning an option here is a little odd in the // case of a non parsable state. @@ -225,18 +103,6 @@ pub trait FileExt: AsRawHandle { } } - /// Sets the pin state of the placeholder. - fn set_pin_state(&self, state: PinState, options: PinOptions) -> core::Result<()> { - unsafe { - CfSetPinState( - HANDLE(self.as_raw_handle() as isize), - state.into(), - options.0, - ptr::null_mut(), - ) - } - } - /// Marks a placeholder as synced. /// /// If the passed [USN][crate::Usn] is outdated, the call will fail. @@ -272,7 +138,7 @@ pub trait FileExt: AsRawHandle { unsafe { CfGetSyncRootInfoByHandle( HANDLE(self.as_raw_handle() as isize), - CF_SYNC_ROOT_INFO_STANDARD, + CloudFilters::CF_SYNC_ROOT_INFO_STANDARD, data.as_mut_ptr() as *mut _, data.len() as u32, ptr::null_mut(), @@ -495,260 +361,6 @@ impl From for CF_SYNC_PROVIDER_STATUS { } } -/// The pin state of a placeholder. -/// -/// [Read more -/// here](https://docs.microsoft.com/en-us/windows/win32/api/cfapi/ne-cfapi-cf_pin_state#remarks) -#[derive(Debug, Clone, Copy)] -pub enum PinState { - /// The platform could decide freely. - Unspecified, - /// [SyncFilter::fetch_data][crate::SyncFilter::fetch_data] will be called to hydrate the rest - /// of the placeholder's data. Any dehydration requests will fail automatically. - Pinned, - /// [SyncFilter::dehydrate][crate::SyncFilter::dehydrate] will be called to dehydrate the rest - /// of the placeholder's data. - Unpinned, - /// The placeholder will never sync to the cloud. - Excluded, - /// The placeholder will inherit the parent placeholder's pin state. - Inherit, -} - -impl From for CF_PIN_STATE { - fn from(state: PinState) -> Self { - match state { - PinState::Unspecified => CloudFilters::CF_PIN_STATE_UNSPECIFIED, - PinState::Pinned => CloudFilters::CF_PIN_STATE_PINNED, - PinState::Unpinned => CloudFilters::CF_PIN_STATE_UNPINNED, - PinState::Excluded => CloudFilters::CF_PIN_STATE_EXCLUDED, - PinState::Inherit => CloudFilters::CF_PIN_STATE_INHERIT, - } - } -} - -impl From for PinState { - fn from(state: CF_PIN_STATE) -> Self { - match state { - CloudFilters::CF_PIN_STATE_UNSPECIFIED => PinState::Unspecified, - CloudFilters::CF_PIN_STATE_PINNED => PinState::Pinned, - CloudFilters::CF_PIN_STATE_UNPINNED => PinState::Unpinned, - CloudFilters::CF_PIN_STATE_EXCLUDED => PinState::Excluded, - CloudFilters::CF_PIN_STATE_INHERIT => PinState::Inherit, - _ => unreachable!(), - } - } -} - -/// The placeholder pin flags. -#[derive(Debug, Clone, Copy)] -pub struct PinOptions(CF_SET_PIN_FLAGS); - -impl PinOptions { - /// Applies the pin state to all descendants of the placeholder (if the placeholder is a - /// directory). - pub fn pin_descendants(&mut self) -> &mut Self { - self.0 |= CloudFilters::CF_SET_PIN_FLAG_RECURSE; - self - } - - /// Applies the pin state to all descendants of the placeholder excluding the current one (if - /// the placeholder is a directory). - pub fn pin_descendants_not_self(&mut self) -> &mut Self { - self.0 |= CloudFilters::CF_SET_PIN_FLAG_RECURSE_ONLY; - self - } - - /// Stop applying the pin state when the first error is encountered. Otherwise, skip over it - /// and keep applying. - pub fn stop_on_error(&mut self) -> &mut Self { - self.0 |= CloudFilters::CF_SET_PIN_FLAG_RECURSE_STOP_ON_ERROR; - self - } -} - -impl Default for PinOptions { - fn default() -> Self { - Self(CloudFilters::CF_SET_PIN_FLAG_NONE) - } -} - -/// File to placeholder file conversion parameters. -#[derive(Debug, Clone)] -pub struct ConvertOptions<'a> { - flags: CF_CONVERT_FLAGS, - blob: Option<&'a [u8]>, -} - -impl<'a> ConvertOptions<'a> { - /// Marks the placeholder as synced. - /// - /// This flag is used to determine the status of a placeholder shown in the file explorer. It - /// is applicable to both files and directories. - /// - /// A file or directory should be marked as "synced" when it has all of its data and metadata. - /// A file that is partially full could still be marked as synced, any remaining data will - /// invoke the [SyncFilter::fetch_data][crate::SyncFilter::fetch_data] callback automatically - /// if requested. - pub fn mark_sync(mut self) -> Self { - self.flags |= CloudFilters::CF_CONVERT_FLAG_MARK_IN_SYNC; - self - } - - /// Dehydrate the placeholder after conversion. - /// - /// This flag is only applicable to files. - pub fn dehydrate(mut self) -> Self { - self.flags |= CloudFilters::CF_CONVERT_FLAG_DEHYDRATE; - self - } - - // TODO: make the name of this function more specific - /// Marks the placeholder as "partially full," such that [SyncFilter::fetch_placeholders][crate::SyncFilter::fetch_placeholders] - /// will be invoked when this directory is next accessed so that the remaining placeholders are inserted. - /// - /// Only applicable to placeholder directories. - pub fn has_children(mut self) -> Self { - self.flags |= CloudFilters::CF_CONVERT_FLAG_ENABLE_ON_DEMAND_POPULATION; - self - } - - /// Blocks this placeholder from being dehydrated. - /// - /// This flag does not work on directories. - pub fn block_dehydration(mut self) -> Self { - self.flags |= CloudFilters::CF_CONVERT_FLAG_ALWAYS_FULL; - self - } - - /// Forces the conversion of a non-cloud placeholder file to a cloud placeholder file. - /// - /// Placeholder files are built into the NTFS file system and thus, a placeholder not associated - /// with the sync root is possible. - pub fn force(mut self) -> Self { - self.flags |= CloudFilters::CF_CONVERT_FLAG_FORCE_CONVERT_TO_CLOUD_FILE; - self - } - - /// A buffer of bytes stored with the file that could be accessed through a - /// [Request::file_blob][crate::Request::file_blob] or - /// [FileExit::placeholder_info][crate::ext::FileExt::placeholder_info]. - /// - /// The buffer must not exceed - /// [4KiB](https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/Storage/CloudFilters/constant.CF_PLACEHOLDER_MAX_FILE_IDENTITY_LENGTH.html). - pub fn blob(mut self, blob: &'a [u8]) -> Self { - assert!( - blob.len() <= CloudFilters::CF_PLACEHOLDER_MAX_FILE_IDENTITY_LENGTH as usize, - "blob size must not exceed {} bytes, got {} bytes", - CloudFilters::CF_PLACEHOLDER_MAX_FILE_IDENTITY_LENGTH, - blob.len() - ); - self.blob = Some(blob); - self - } -} - -impl Default for ConvertOptions<'_> { - fn default() -> Self { - Self { - flags: CloudFilters::CF_CONVERT_FLAG_NONE, - blob: None, - } - } -} - -/// Placeholder update parameters. -#[derive(Debug, Clone)] -pub struct UpdateOptions<'a> { - metadata: Option, - dehydrate_range: Vec, - flags: CF_UPDATE_FLAGS, - blob: Option<&'a [u8]>, -} - -impl<'a> UpdateOptions<'a> { - pub fn metadata(mut self, metadata: Metadata) -> Self { - self.metadata = Some(metadata); - self - } - - // TODO: user should be able to specify an array of RangeBounds - pub fn dehydrate_range(mut self, range: Range) -> Self { - self.dehydrate_range.push(CF_FILE_RANGE { - StartingOffset: range.start as i64, - Length: range.end as i64, - }); - self - } - - pub fn update_if_synced(mut self) -> Self { - self.flags |= CloudFilters::CF_UPDATE_FLAG_VERIFY_IN_SYNC; - self - } - - pub fn mark_sync(mut self) -> Self { - self.flags |= CloudFilters::CF_UPDATE_FLAG_MARK_IN_SYNC; - self - } - - // files only - pub fn dehydrate(mut self) -> Self { - self.flags |= CloudFilters::CF_UPDATE_FLAG_DEHYDRATE; - self - } - - // directories only - pub fn children_present(mut self) -> Self { - self.flags |= CloudFilters::CF_UPDATE_FLAG_DISABLE_ON_DEMAND_POPULATION; - self - } - - pub fn remove_blob(mut self) -> Self { - self.flags |= CloudFilters::CF_UPDATE_FLAG_REMOVE_FILE_IDENTITY; - self - } - - pub fn mark_unsync(mut self) -> Self { - self.flags |= CloudFilters::CF_UPDATE_FLAG_CLEAR_IN_SYNC; - self - } - - // TODO: what does this do? - pub fn remove_properties(mut self) -> Self { - self.flags |= CloudFilters::CF_UPDATE_FLAG_REMOVE_PROPERTY; - self - } - - // TODO: this doesn't seem necessary - pub fn skip_0_metadata_fields(mut self) -> Self { - self.flags |= CloudFilters::CF_UPDATE_FLAG_PASSTHROUGH_FS_METADATA; - self - } - - pub fn blob(mut self, blob: &'a [u8]) -> Self { - assert!( - blob.len() <= CloudFilters::CF_PLACEHOLDER_MAX_FILE_IDENTITY_LENGTH as usize, - "blob size must not exceed {} bytes, got {} bytes", - CloudFilters::CF_PLACEHOLDER_MAX_FILE_IDENTITY_LENGTH, - blob.len() - ); - self.blob = Some(blob); - self - } -} - -impl Default for UpdateOptions<'_> { - fn default() -> Self { - Self { - metadata: None, - dehydrate_range: Vec::new(), - flags: CloudFilters::CF_UPDATE_FLAG_NONE - | CloudFilters::CF_UPDATE_FLAG_ENABLE_ON_DEMAND_POPULATION, - blob: None, - } - } -} - // TODO: I don't think this is an enum #[derive(Debug, Clone, Copy)] pub enum PlaceholderState { @@ -777,45 +389,3 @@ impl PlaceholderState { } } } - -#[derive(Debug)] -pub struct PlaceholderInfo { - data: Vec, - info: *const CF_PLACEHOLDER_STANDARD_INFO, -} - -impl PlaceholderInfo { - pub fn on_disk_data_size(&self) -> u64 { - unsafe { &*self.info }.OnDiskDataSize as u64 - } - - pub fn validated_data_size(&self) -> u64 { - unsafe { &*self.info }.ValidatedDataSize as u64 - } - pub fn modified_data_size(&self) -> u64 { - unsafe { &*self.info }.ModifiedDataSize as u64 - } - pub fn properties_size(&self) -> u64 { - unsafe { &*self.info }.PropertiesSize as u64 - } - - pub fn pin_state(&self) -> PinState { - unsafe { &*self.info }.PinState.into() - } - - pub fn is_synced(&self) -> bool { - unsafe { &*self.info }.InSyncState == CloudFilters::CF_IN_SYNC_STATE_IN_SYNC - } - - pub fn file_id(&self) -> i64 { - unsafe { &*self.info }.FileId - } - - pub fn sync_root_file_id(&self) -> i64 { - unsafe { &*self.info }.SyncRootFileId - } - - pub fn blob(&self) -> &[u8] { - &self.data[mem::size_of::()..] - } -} diff --git a/src/ext/mod.rs b/src/ext/mod.rs index e417a48..333c997 100644 --- a/src/ext/mod.rs +++ b/src/ext/mod.rs @@ -1,8 +1,5 @@ mod file; mod path; -pub use file::{ - ConvertOptions, FileExt, PinOptions, PinState, PlaceholderInfo, PlaceholderState, - ProviderStatus, SyncRootInfo, UpdateOptions, -}; +pub use file::{FileExt, PlaceholderState, ProviderStatus, SyncRootInfo}; pub use path::PathExt; diff --git a/src/filter/ticket.rs b/src/filter/ticket.rs index 5514757..e356662 100644 --- a/src/filter/ticket.rs +++ b/src/filter/ticket.rs @@ -33,6 +33,10 @@ impl FetchData { command::Write::fail(self.connection_key, self.transfer_key, error_kind) } + /// Displays a progress bar next to the file in the file explorer to show the progress of the + /// current operation. In addition, the standard Windows file progress dialog will open + /// displaying the speed and progress based on the values set. During background hydrations, + /// an interactive toast will appear notifying the user of an operation with a progress bar. pub fn report_progress(&self, total: u64, completed: u64) -> core::Result<()> { unsafe { CfReportProviderProgress( @@ -45,6 +49,8 @@ impl FetchData { Ok(()) } + + // TODO: response Command::Update } impl utility::ReadAt for FetchData { @@ -94,10 +100,15 @@ impl ValidateData { } } + /// Validates the data range in the placeholder file is valid. + /// + /// This method should be used in the + /// [SyncFilter::validate_data][crate::SyncFilter::validate_data] callback. + /// + /// This method is equivalent to calling `CfExecute` with `CF_OPERATION_TYPE_ACK_DATA`. // TODO: make this generic over a RangeBounds // if the range specified is past the current file length, will it consider that range to be validated? // https://docs.microsoft.com/en-us/answers/questions/750302/if-the-ackdata-field-of-cf-operation-parameters-is.html - /// Confirms the specified range in the file is valid. pub fn pass(&self, range: Range) -> core::Result<()> { command::Validate { range }.execute(self.connection_key, self.transfer_key) } @@ -106,6 +117,8 @@ impl ValidateData { pub fn fail(&self, error_kind: CloudErrorKind) -> core::Result<()> { command::Validate::fail(self.connection_key, self.transfer_key, error_kind) } + + // TODO: response Command::Update } impl utility::ReadAt for ValidateData { diff --git a/src/lib.rs b/src/lib.rs index efccf03..a5bb0b6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,7 @@ pub mod utility; pub use error::CloudErrorKind; pub use filter::{info, ticket, SyncFilter}; -pub use placeholder::{Placeholder, UpdateOptions}; +pub use placeholder::Placeholder; pub use placeholder_file::{BatchCreate, Metadata, PlaceholderFile}; pub use request::{Process, Request}; pub use root::{ diff --git a/src/placeholder.rs b/src/placeholder.rs index 67357bc..93dac94 100644 --- a/src/placeholder.rs +++ b/src/placeholder.rs @@ -1,329 +1,652 @@ use std::{ - io::{self, Seek, SeekFrom}, - mem::ManuallyDrop, + fs::File, + mem, ops::Range, - path::{Path, PathBuf}, + os::windows::io::{FromRawHandle, IntoRawHandle}, + path::Path, ptr, }; use widestring::U16CString; use windows::{ - core::{self, GUID}, + core::{self, PCWSTR}, Win32::{ - Storage::{ - CloudFilters::{self, CfReportProviderProgress, CF_CONNECTION_KEY}, - EnhancedStorage, - }, - System::{ - Com::StructuredStorage::{ - PROPVARIANT, PROPVARIANT_0, PROPVARIANT_0_0, PROPVARIANT_0_0_0, - }, - Ole::VT_UI4, - }, - UI::Shell::{ - self, IShellItem2, - PropertiesSystem::{ - self, IPropertyStore, InitPropVariantFromUInt64Vector, PROPERTYKEY, - }, - SHChangeNotify, SHCreateItemFromParsingName, + Foundation::{CloseHandle, ERROR_NOT_A_CLOUD_FILE, HANDLE}, + Storage::CloudFilters::{ + self, CfCloseHandle, CfConvertToPlaceholder, CfGetPlaceholderInfo, + CfOpenFileWithOplock, CfRevertPlaceholder, CfSetInSyncState, CfSetPinState, + CfUpdatePlaceholder, CF_CONVERT_FLAGS, CF_FILE_RANGE, CF_OPEN_FILE_FLAGS, CF_PIN_STATE, + CF_PLACEHOLDER_STANDARD_INFO, CF_SET_PIN_FLAGS, CF_UPDATE_FLAGS, }, }, }; -use crate::{ - command::{Command, Read, Update, Validate, Write}, - placeholder_file::Metadata, - request::{RawConnectionKey, RawTransferKey}, -}; +use crate::{Metadata, Usn}; -// secret PKEY -const STORAGE_PROVIDER_TRANSFER_PROGRESS: PROPERTYKEY = PROPERTYKEY { - fmtid: GUID::from_values( - 0xE77E90DF, - 0x6271, - 0x4F5B, - [0x83, 0x4F, 0x2D, 0xD1, 0xF2, 0x45, 0xDD, 0xA4], - ), - pid: 4, -}; +/// The type of handle that the placeholder file/directory owns. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PlaceholderHandleType { + /// A handle that was opened with [CfOpenFileWithOplock]. + CfApi, + /// A handle that was opened with [CreateFile] etc. + Win32, +} -/// A struct to perform various operations on a placeholder file/directory. -#[derive(Debug, Clone)] -pub struct Placeholder { - connection_key: RawConnectionKey, - transfer_key: RawTransferKey, - // TODO: take in a borrowed path - path: PathBuf, - // TODO: how does file size behave when writing past the last recorded file size? - file_size: u64, - position: u64, +/// An owned handle to a placeholder file/directory. +/// +/// This closes the handle on drop. +#[derive(Debug)] +pub struct OwnedPlaceholderHandle { + handle_type: PlaceholderHandleType, + handle: HANDLE, } -impl Placeholder { - pub(crate) fn new( - connection_key: RawConnectionKey, - transfer_key: RawTransferKey, - path: PathBuf, - file_size: u64, - ) -> Self { +impl OwnedPlaceholderHandle { + /// Create a new [OwnedPlaceholderHandle] from a handle returned by [CfOpenFileWithOplock]. + /// + /// # Safety + /// + /// The handle must be valid and owned by the caller. + pub unsafe fn from_cfapi(handle: HANDLE) -> Self { Self { - connection_key, - transfer_key, - path, - file_size, - position: 0, + handle_type: PlaceholderHandleType::CfApi, + handle, } } - // TODO: this function is a bit of a pickle - // if I let the user pass in the transfer key, then I somehow need to get the file path to support set_progress - // if I allow getting a transfer key from a path, then I somehow need to let them specify read/write access (probably?) - pub fn from_path>( - _connection_key: RawConnectionKey, - _path: P, - ) -> core::Result { - // let file = File::open(path).unwrap(); - // let key = unsafe { CfGetTransferKey(HANDLE(file.as_raw_handle() as isize))?}; - // OwnedTransferKey::new(key, file); + /// Create a new [OwnedPlaceholderHandle] from a handle returned by [CreateFile] etc. + /// + /// # Safety + /// + /// The handle must be valid and owned by the caller. + pub unsafe fn from_win32(handle: HANDLE) -> Self { + Self { + handle_type: PlaceholderHandleType::Win32, + handle, + } + } - // Ok(Self { - // connection_key, + pub const fn handle(&self) -> HANDLE { + self.handle + } - // }) - todo!() + pub const fn handle_type(&self) -> PlaceholderHandleType { + self.handle_type } +} - /// Validates the data range in the placeholder file is valid. - /// - /// This method should be used in the - /// [SyncFilter::validate_data][crate::SyncFilter::validate_data] callback. - /// - /// This method is equivalent to [Validate::execute][crate::command::Validate::execute]. - // TODO: accept a generic RangeBounds - pub fn validate(&self, range: Range) -> core::Result<()> { - Validate { range }.execute(self.connection_key, self.transfer_key) +impl Drop for OwnedPlaceholderHandle { + fn drop(&mut self) { + match self.handle_type { + PlaceholderHandleType::CfApi => unsafe { CfCloseHandle(self.handle) }, + PlaceholderHandleType::Win32 => unsafe { + CloseHandle(self.handle); + }, + } } +} - /// Updates various properties on a placeholder. - /// - /// This method is equivalent to calling [Update::execute][crate::command::Update::execute]. - pub fn update(&self, options: UpdateOptions) -> core::Result<()> { - options.0.execute(self.connection_key, self.transfer_key) +/// Options for opening a placeholder file/directory. +pub struct OpenOptions { + flags: CF_OPEN_FILE_FLAGS, +} + +impl OpenOptions { + pub fn new() -> Self { + Self::default() } - /// Shortcut for calling [Placeholder::update][crate::Placeholder::update] with - /// [UpdateOptions::mark_sync][crate::UpdateOptions::mark_sync]. - pub fn mark_sync(&self) -> core::Result<()> { - self.update(UpdateOptions::new().mark_sync()) + pub fn exclusive(mut self) -> Self { + self.flags |= CloudFilters::CF_OPEN_FILE_FLAG_EXCLUSIVE; + self } - /// Shortcut for calling [Placeholder::update][crate::Placeholder::update] with - /// [UpdateOptions::metadata][crate::UpdateOptions::metadata]. - pub fn set_metadata(&self, metadata: Metadata) -> core::Result<()> { - self.update(UpdateOptions::new().metadata(metadata)) + pub fn write_access(mut self) -> Self { + self.flags |= CloudFilters::CF_OPEN_FILE_FLAG_WRITE_ACCESS; + self } - /// Shortcut for calling [Placeholder::update][crate::Placeholder::update] with - /// [UpdateOptions::blob][crate::UpdateOptions::blob]. - pub fn set_blob(self, blob: &[u8]) -> core::Result<()> { - self.update(UpdateOptions::new().blob(blob)) + pub fn delete_access(mut self) -> Self { + self.flags |= CloudFilters::CF_OPEN_FILE_FLAG_DELETE_ACCESS; + self } - /// Displays a progress bar next to the file in the file explorer to show the progress of the - /// current operation. In addition, the standard Windows file progress dialog will open - /// displaying the speed and progress based on the values set. During background hydrations, - /// an interactive toast will appear notifying the user of an operation with a progress bar. - // TODO: Add gifs to the docs to show each type of display - pub fn set_progress(&self, total: u64, completed: u64) -> core::Result<()> { - unsafe { - CfReportProviderProgress( - CF_CONNECTION_KEY(self.connection_key), - self.transfer_key, - total as i64, - completed as i64, - )?; - - let item: IShellItem2 = SHCreateItemFromParsingName(self.path.as_os_str(), None)?; - let store: IPropertyStore = item.GetPropertyStore( - PropertiesSystem::GPS_READWRITE | PropertiesSystem::GPS_VOLATILEPROPERTIESONLY, - )?; - - let progress = InitPropVariantFromUInt64Vector(&mut [completed, total] as *mut _, 2)?; - store.SetValue( - &STORAGE_PROVIDER_TRANSFER_PROGRESS as *const _, - &progress as *const _, - )?; - - let status = InitPropVariantFromUInt32(if completed < total { - PropertiesSystem::STS_TRANSFERRING.0 - } else { - PropertiesSystem::STS_NONE.0 - }); - store.SetValue( - &EnhancedStorage::PKEY_SyncTransferStatus as *const _, - &status as *const _, - )?; - - store.Commit()?; - - SHChangeNotify( - Shell::SHCNE_UPDATEITEM, - Shell::SHCNF_PATHW, - U16CString::from_os_str_unchecked(self.path.as_os_str()).as_ptr() as *const _, - ptr::null_mut(), - ); + pub fn foreground(mut self) -> Self { + self.flags |= CloudFilters::CF_OPEN_FILE_FLAG_FOREGROUND; + self + } + + /// Open the placeholder file/directory using `CfOpenFileWithOplock`. + pub fn open(self, path: impl AsRef) -> core::Result { + let u16_path = U16CString::from_os_str(path.as_ref()).unwrap(); + let handle = unsafe { CfOpenFileWithOplock(PCWSTR(u16_path.as_ptr()), self.flags) }?; + Ok(Placeholder { + handle: unsafe { OwnedPlaceholderHandle::from_cfapi(handle) }, + }) + } +} - Ok(()) +impl Default for OpenOptions { + fn default() -> Self { + Self { + flags: CloudFilters::CF_OPEN_FILE_FLAG_NONE, } } } -// TODO: does this have the same 4KiB requirement as writing? -impl io::Read for Placeholder { - /// Read data from a placeholder file. - /// - /// This method is equivalent to calling [Read::execute][crate::command::Read::execute], except - /// it will not increment the file cursor. - /// - /// The bytes returned will ALWAYS be the length of the buffer passed in. The operating - /// system provides these guarantees. - // TODO: as far as I know, what I said above is true, double check though - fn read(&mut self, buffer: &mut [u8]) -> io::Result { - let result = Read { - buffer, - position: self.position, +/// The pin state of a placeholder. +/// +/// [Read more +/// here](https://docs.microsoft.com/en-us/windows/win32/api/cfapi/ne-cfapi-cf_pin_state#remarks) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PinState { + /// The platform could decide freely. + Unspecified, + /// [SyncFilter::fetch_data][crate::SyncFilter::fetch_data] will be called to hydrate the rest + /// of the placeholder's data. Any dehydration requests will fail automatically. + Pinned, + /// [SyncFilter::dehydrate][crate::SyncFilter::dehydrate] will be called to dehydrate the rest + /// of the placeholder's data. + Unpinned, + /// The placeholder will never sync to the cloud. + Excluded, + /// The placeholder will inherit the parent placeholder's pin state. + Inherit, +} + +impl From for CF_PIN_STATE { + fn from(state: PinState) -> Self { + match state { + PinState::Unspecified => CloudFilters::CF_PIN_STATE_UNSPECIFIED, + PinState::Pinned => CloudFilters::CF_PIN_STATE_PINNED, + PinState::Unpinned => CloudFilters::CF_PIN_STATE_UNPINNED, + PinState::Excluded => CloudFilters::CF_PIN_STATE_EXCLUDED, + PinState::Inherit => CloudFilters::CF_PIN_STATE_INHERIT, } - .execute(self.connection_key, self.transfer_key); + } +} - match result { - Ok(bytes_read) => { - self.position += bytes_read; - Ok(bytes_read as usize) - } - Err(err) => Err(err.into()), +impl From for PinState { + fn from(state: CF_PIN_STATE) -> Self { + match state { + CloudFilters::CF_PIN_STATE_UNSPECIFIED => PinState::Unspecified, + CloudFilters::CF_PIN_STATE_PINNED => PinState::Pinned, + CloudFilters::CF_PIN_STATE_UNPINNED => PinState::Unpinned, + CloudFilters::CF_PIN_STATE_EXCLUDED => PinState::Excluded, + CloudFilters::CF_PIN_STATE_INHERIT => PinState::Inherit, + _ => unreachable!(), } } } -impl io::Write for Placeholder { - /// Write data to a placeholder. +/// The placeholder pin flags. +#[derive(Debug, Clone, Copy)] +pub struct PinOptions(CF_SET_PIN_FLAGS); + +impl PinOptions { + /// Applies the pin state to all descendants of the placeholder (if the placeholder is a + /// directory). + pub fn recurse(&mut self) -> &mut Self { + self.0 |= CloudFilters::CF_SET_PIN_FLAG_RECURSE; + self + } + + /// Applies the pin state to all descendants of the placeholder excluding the current one (if + /// the placeholder is a directory). + pub fn recurse_children(&mut self) -> &mut Self { + self.0 |= CloudFilters::CF_SET_PIN_FLAG_RECURSE_ONLY; + self + } + + /// Stop applying the pin state when the first error is encountered. Otherwise, skip over it + /// and keep applying. + pub fn stop_on_error(&mut self) -> &mut Self { + self.0 |= CloudFilters::CF_SET_PIN_FLAG_RECURSE_STOP_ON_ERROR; + self + } +} + +impl Default for PinOptions { + fn default() -> Self { + Self(CloudFilters::CF_SET_PIN_FLAG_NONE) + } +} + +/// File to placeholder file conversion parameters. +#[derive(Debug, Clone)] +pub struct ConvertOptions { + flags: CF_CONVERT_FLAGS, + blob: Vec, +} + +impl ConvertOptions { + /// Marks a placeholder as in sync. /// - /// The buffer passed must be 4KiB in length or end on the logical file size. Unfortunately, - /// this is a restriction of the operating system. Read - /// [here](https://github.com/ok-nick/wincs/issues/3) for a convenient abstraction. + /// See also + /// [SetInSyncState](https://learn.microsoft.com/en-us/windows/win32/api/cfapi/nf-cfapi-cfsetinsyncstate), + /// [What does "In-Sync" Mean?](https://www.userfilesystem.com/programming/faq/#nav_whatdoesin-syncmean) + pub fn mark_in_sync(mut self) -> Self { + self.flags |= CloudFilters::CF_CONVERT_FLAG_MARK_IN_SYNC; + self + } + + /// Dehydrate the placeholder after conversion. /// - /// The bytes returned will ALWAYS be the length of the buffer passed in. The operating system - /// provides these guarantees. + /// This flag is only applicable to files. + pub fn dehydrate(mut self) -> Self { + self.flags |= CloudFilters::CF_CONVERT_FLAG_DEHYDRATE; + self + } + + // TODO: make the name of this function more specific + /// Marks the placeholder as "partially full," such that [SyncFilter::fetch_placeholders][crate::SyncFilter::fetch_placeholders] + /// will be invoked when this directory is next accessed so that the remaining placeholders are inserted. + /// + /// Only applicable to placeholder directories. + pub fn has_children(mut self) -> Self { + self.flags |= CloudFilters::CF_CONVERT_FLAG_ENABLE_ON_DEMAND_POPULATION; + self + } + + /// Blocks this placeholder from being dehydrated. /// - /// This method is equivalent to calling [Write::execute][crate::command::Write::execute], except it will not increment the - /// file cursor. - // TODO: confirm the abovementioned is true - fn write(&mut self, buffer: &[u8]) -> io::Result { + /// This flag does not work on directories. + pub fn block_dehydration(mut self) -> Self { + self.flags |= CloudFilters::CF_CONVERT_FLAG_ALWAYS_FULL; + self + } + + /// Forces the conversion of a non-cloud placeholder file to a cloud placeholder file. + /// + /// Placeholder files are built into the NTFS file system and thus, a placeholder not associated + /// with the sync root is possible. + pub fn force(mut self) -> Self { + self.flags |= CloudFilters::CF_CONVERT_FLAG_FORCE_CONVERT_TO_CLOUD_FILE; + self + } + + /// A buffer of bytes stored with the file that could be accessed through a + /// [Request::file_blob][crate::Request::file_blob] or + /// [FileExit::placeholder_info][crate::ext::FileExt::placeholder_info]. + /// + /// The buffer must not exceed + /// [4KiB](https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/Storage/CloudFilters/constant.CF_PLACEHOLDER_MAX_FILE_IDENTITY_LENGTH.html). + pub fn blob(mut self, blob: Vec) -> Self { assert!( - buffer.len() % 4096 == 0 || self.position + buffer.len() as u64 >= self.file_size, - "the length of the buffer must be 4KiB aligned or ending on the logical file size" + blob.len() <= CloudFilters::CF_PLACEHOLDER_MAX_FILE_IDENTITY_LENGTH as usize, + "blob size must not exceed {} bytes, got {} bytes", + CloudFilters::CF_PLACEHOLDER_MAX_FILE_IDENTITY_LENGTH, + blob.len() ); + self.blob = blob; + self + } +} - let result = Write { - buffer, - position: self.position, +impl Default for ConvertOptions { + fn default() -> Self { + Self { + flags: CloudFilters::CF_CONVERT_FLAG_NONE, + blob: Vec::new(), } - .execute(self.connection_key, self.transfer_key); + } +} - match result { - Ok(_) => { - self.position += buffer.len() as u64; - Ok(buffer.len()) - } - Err(err) => Err(err.into()), - } +#[derive(Clone)] +pub struct PlaceholderInfo { + data: Vec, + info: *const CF_PLACEHOLDER_STANDARD_INFO, +} + +impl PlaceholderInfo { + pub fn on_disk_data_size(&self) -> i64 { + unsafe { &*self.info }.OnDiskDataSize } - /// This does not do anything. - fn flush(&mut self) -> io::Result<()> { - Ok(()) + pub fn validated_data_size(&self) -> i64 { + unsafe { &*self.info }.ValidatedDataSize + } + pub fn modified_data_size(&self) -> i64 { + unsafe { &*self.info }.ModifiedDataSize + } + pub fn properties_size(&self) -> i64 { + unsafe { &*self.info }.PropertiesSize } -} -// TODO: properly handle seeking -impl Seek for Placeholder { - fn seek(&mut self, position: SeekFrom) -> io::Result { - self.position = match position { - SeekFrom::Start(offset) => offset, - SeekFrom::Current(offset) => self.position + offset as u64, - SeekFrom::End(offset) => self.file_size + offset as u64, - }; + pub fn pin_state(&self) -> PinState { + unsafe { &*self.info }.PinState.into() + } + + pub fn is_in_sync(&self) -> bool { + unsafe { &*self.info }.InSyncState == CloudFilters::CF_IN_SYNC_STATE_IN_SYNC + } - Ok(self.position) + pub fn file_id(&self) -> i64 { + unsafe { &*self.info }.FileId + } + + pub fn sync_root_file_id(&self) -> i64 { + unsafe { &*self.info }.SyncRootFileId + } + + pub fn blob(&self) -> &[u8] { + &self.data[mem::size_of::()..] } } -/// Various properties to update a placeholder in batch. -#[derive(Debug)] -pub struct UpdateOptions<'a>(Update<'a>); +impl std::fmt::Debug for PlaceholderInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PlaceholderInfo") + .field("on_disk_data_size", &self.on_disk_data_size()) + .field("validated_data_size", &self.validated_data_size()) + .field("modified_data_size", &self.modified_data_size()) + .field("properties_size", &self.properties_size()) + .field("pin_state", &self.pin_state()) + .field("is_in_sync", &self.is_in_sync()) + .field("file_id", &self.file_id()) + .field("sync_root_file_id", &self.sync_root_file_id()) + .finish() + } +} + +/// Placeholder update parameters. +#[derive(Debug, Clone)] +pub struct UpdateOptions<'a> { + metadata: Option, + dehydrate_ranges: Vec, + flags: CF_UPDATE_FLAGS, + blob: &'a [u8], +} impl<'a> UpdateOptions<'a> { - /// Create a new [UpdateOptions][crate::UpdateOptions]. - pub fn new() -> Self { - Self::default() + /// [Metadata][crate::Metadata] contains file system metadata about the placeholder to be updated. + /// + /// File size will be truncates to 0 if not specified, otherwise to the specified size. + pub fn metadata(mut self, metadata: Metadata) -> Self { + self.flags &= !(CloudFilters::CF_UPDATE_FLAG_PASSTHROUGH_FS_METADATA); + self.metadata = Some(metadata); + self + } + + /// Fields in [Metadata][crate::Metadata] will be updated. + pub fn metadata_all(mut self, metadata: Metadata) -> Self { + self.flags |= CloudFilters::CF_UPDATE_FLAG_PASSTHROUGH_FS_METADATA; + self.metadata = Some(metadata); + self } - /// Marks the placeholder as synced. + /// Extended ranges to be dehydrated. /// - /// A file or directory should be marked as "synced" when it has all of its data and metadata. - /// A file that is partially full could still be marked as synced, any remaining data will - /// invoke the [SyncFilter::fetch_data][crate::SyncFilter::fetch_data] callback automatically - /// if requested. - pub fn mark_sync(mut self) -> Self { - self.0.mark_sync = true; + /// All the offsets and lengths should be `PAGE_SIZE` aligned. + /// Passing a single range with Offset `0` and Length `CF_EOF` will invalidate the entire file. + /// This has the same effect as passing the flag `CF_UPDATE_FLAG_DEHYDRATE` instead + pub fn dehydrate_ranges(mut self, ranges: impl IntoIterator>) -> Self { + self.dehydrate_ranges + .extend(ranges.into_iter().map(|r| CF_FILE_RANGE { + StartingOffset: r.start as _, + Length: (r.end - r.start) as _, + })); self } - /// The metadata for the placeholder. - pub fn metadata(mut self, metadata: Metadata) -> Self { - self.0.metadata = Some(metadata); + /// The update will fail if the `IN_SYNC` attribute is not currently set on the placeholder. + pub fn update_if_in_sync(mut self) -> Self { + self.flags |= CloudFilters::CF_UPDATE_FLAG_VERIFY_IN_SYNC; self } - /// A buffer of bytes stored with the file that could be accessed through - /// [Request::file_blob][crate::Request::file_blob] or - /// [FileExt::placeholder_info][crate::ext::FileExt::placeholder_info]. + /// Marks a placeholder as in sync. /// - /// The buffer must not exceed - /// [4KiB](https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/Storage/CloudFilters/constant.CF_PLACEHOLDER_MAX_FILE_IDENTITY_LENGTH.html). + /// See also + /// [SetInSyncState](https://learn.microsoft.com/en-us/windows/win32/api/cfapi/nf-cfapi-cfsetinsyncstate), + /// [What does "In-Sync" Mean?](https://www.userfilesystem.com/programming/faq/#nav_whatdoesin-syncmean) + pub fn mark_in_sync(mut self) -> Self { + self.flags |= CloudFilters::CF_UPDATE_FLAG_MARK_IN_SYNC; + self + } + + /// Marks a placeholder as not in sync. `Sync Pending` will be shown in explorer. + /// + /// See also + /// [SetInSyncState](https://learn.microsoft.com/en-us/windows/win32/api/cfapi/nf-cfapi-cfsetinsyncstate), + /// [What does "In-Sync" Mean?](https://www.userfilesystem.com/programming/faq/#nav_whatdoesin-syncmean) + pub fn mark_not_in_sync(mut self) -> Self { + self.flags |= CloudFilters::CF_UPDATE_FLAG_CLEAR_IN_SYNC; + self + } + + /// The platform dehydrates the file after updating the placeholder successfully. + pub fn dehydrate(mut self) -> Self { + self.flags |= CloudFilters::CF_UPDATE_FLAG_DEHYDRATE; + self + } + + /// Disables on-demand population for directories. + pub fn no_more_children(mut self) -> Self { + self.flags |= CloudFilters::CF_UPDATE_FLAG_DISABLE_ON_DEMAND_POPULATION; + self + } + + /// Enable on-demand population for directories. + pub fn has_more_children(mut self) -> Self { + self.flags |= CloudFilters::CF_UPDATE_FLAG_ENABLE_ON_DEMAND_POPULATION; + self + } + + /// Remove the file identity from the placeholder. + /// [UpdateOptions::blob()](crate::placeholder::UpdateOptions::blob) will be ignored. + pub fn remove_blob(mut self) -> Self { + self.flags |= CloudFilters::CF_UPDATE_FLAG_REMOVE_FILE_IDENTITY; + self + } + + /// The platform removes all existing extrinsic properties on the placeholder. + pub fn remove_properties(mut self) -> Self { + self.flags |= CloudFilters::CF_UPDATE_FLAG_REMOVE_PROPERTY; + self + } + pub fn blob(mut self, blob: &'a [u8]) -> Self { assert!( blob.len() <= CloudFilters::CF_PLACEHOLDER_MAX_FILE_IDENTITY_LENGTH as usize, - "blob size must not exceed {} byes, got {} bytes", + "blob size must not exceed {} bytes, got {} bytes", CloudFilters::CF_PLACEHOLDER_MAX_FILE_IDENTITY_LENGTH, blob.len() ); - self.0.blob = blob; + self.blob = blob; self } } -impl<'a> Default for UpdateOptions<'a> { +impl Default for UpdateOptions<'_> { fn default() -> Self { - Self(Update { - mark_sync: false, + Self { metadata: None, + dehydrate_ranges: Vec::new(), + flags: CloudFilters::CF_UPDATE_FLAG_NONE, blob: &[], - }) + } } } -// Equivalent to https://docs.microsoft.com/en-us/windows/win32/api/propvarutil/nf-propvarutil-initpropvariantfromuint32 -// windows-rs doesn't provide bindings to inlined functions -#[allow(non_snake_case)] -fn InitPropVariantFromUInt32(ulVal: u32) -> PROPVARIANT { - PROPVARIANT { - Anonymous: PROPVARIANT_0 { - Anonymous: ManuallyDrop::new(PROPVARIANT_0_0 { - vt: VT_UI4.0 as u16, - Anonymous: PROPVARIANT_0_0_0 { ulVal }, - ..Default::default() - }), - }, +/// A struct to perform various operations on a placeholder(or regular) file/directory. +#[derive(Debug)] +pub struct Placeholder { + handle: OwnedPlaceholderHandle, +} + +impl Placeholder { + /// Open options for opening [Placeholder][crate::Placeholder]s. + pub fn options() -> OpenOptions { + OpenOptions::default() + } + + /// Open the placeholder file/directory with `CF_OPEN_FILE_FLAG_NONE`. + pub fn open(path: impl AsRef) -> core::Result { + OpenOptions::new().open(path) + } + + /// Marks a placeholder as in sync or not. + /// + /// If the passed [USN][crate::Usn] is outdated, the call will fail, + /// otherwise the [USN][crate::Usn] will be updated. + /// + /// See also + /// [SetInSyncState](https://learn.microsoft.com/en-us/windows/win32/api/cfapi/nf-cfapi-cfsetinsyncstate), + /// [What does "In-Sync" Mean?](https://www.userfilesystem.com/programming/faq/#nav_whatdoesin-syncmean) + pub fn mark_in_sync<'a>( + &mut self, + in_sync: bool, + usn: impl Into>, + ) -> core::Result<&mut Self> { + unsafe { + CfSetInSyncState( + self.handle.handle, + match in_sync { + true => CloudFilters::CF_IN_SYNC_STATE_IN_SYNC, + false => CloudFilters::CF_IN_SYNC_STATE_NOT_IN_SYNC, + }, + CloudFilters::CF_SET_IN_SYNC_FLAG_NONE, + usn.into() + .map_or(ptr::null_mut(), |x| ptr::read(x) as *mut _), + ) + }?; + + Ok(self) + } + + /// Sets the pin state of the placeholder. + /// + /// See also + /// [CfSetPinState](https://learn.microsoft.com/en-us/windows/win32/api/cfapi/nf-cfapi-cfsetpinstate), + /// [What does "Pinned" Mean?](https://www.userfilesystem.com/programming/faq/#nav_howdoesthealwayskeeponthisdevicemenuworks) + pub fn mark_pin(&mut self, state: PinState, options: PinOptions) -> core::Result<&mut Self> { + unsafe { CfSetPinState(self.handle.handle, state.into(), options.0, ptr::null_mut()) }?; + Ok(self) + } + + /// Converts a file to a placeholder file. + /// + /// If the passed [USN][crate::Usn] is outdated, the call will fail, + /// otherwise the [USN][crate::Usn] will be updated. + /// + /// See also [CfConvertToPlaceholder](https://learn.microsoft.com/en-us/windows/win32/api/cfapi/nf-cfapi-cfconverttoplaceholder). + pub fn convert_to_placeholder<'a>( + &mut self, + options: ConvertOptions, + usn: impl Into>, + ) -> core::Result<&mut Self> { + unsafe { + CfConvertToPlaceholder( + self.handle.handle, + match options.blob.is_empty() { + true => ptr::null(), + false => options.blob.as_ptr() as *const _, + }, + options.blob.len() as _, + options.flags, + usn.into() + .map_or(ptr::null_mut(), |x| ptr::read(x) as *mut _), + ptr::null_mut(), + ) + }?; + + Ok(self) + } + + /// Gets various characteristics of the placeholder. + /// + /// If the `blob_size` not matches the actual size of the blob, + /// the call will returns `HRESULT_FROM_WIN32(ERROR_MORE_DATA)`. + /// Returns `None` if the handle not points to a placeholder. + pub fn info(&self, blob_size: usize) -> core::Result> { + let mut data = vec![0; mem::size_of::() + blob_size]; + + let r = unsafe { + CfGetPlaceholderInfo( + self.handle.handle, + CloudFilters::CF_PLACEHOLDER_INFO_STANDARD, + data.as_mut_ptr() as *mut _, + data.len() as u32, + ptr::null_mut(), + ) + }; + + match r { + Ok(()) => Ok(Some(PlaceholderInfo { + info: &unsafe { + data[..=mem::size_of::()] + .align_to::() + } + .1[0] as *const _, + data, + })), + Err(e) if e.win32_error() == Some(ERROR_NOT_A_CLOUD_FILE) => Ok(None), + Err(e) => Err(e), + } + } + + /// Updates various characteristics of a placeholder. + /// + /// See also [CfUpdatePlaceholder](https://learn.microsoft.com/en-us/windows/win32/api/cfapi/nf-cfapi-cfupdateplaceholder). + pub fn update<'a>( + &mut self, + options: UpdateOptions, + usn: impl Into>, + ) -> core::Result<&mut Self> { + unsafe { + CfUpdatePlaceholder( + self.handle.handle, + options.metadata.map_or(ptr::null(), |x| &x.0 as *const _), + match options.blob.is_empty() { + true => ptr::null(), + false => options.blob.as_ptr() as *const _, + }, + options.blob.len() as _, + match options.dehydrate_ranges.is_empty() { + true => ptr::null(), + false => options.dehydrate_ranges.as_ptr(), + }, + options.dehydrate_ranges.len() as u32, + options.flags, + usn.into() + .map_or(ptr::null_mut(), |x| ptr::read(x) as *mut _), + ptr::null_mut(), + ) + }?; + + Ok(self) + } +} + +impl From for Placeholder { + fn from(file: File) -> Self { + Self { + handle: unsafe { + OwnedPlaceholderHandle::from_win32(HANDLE(file.into_raw_handle() as _)) + }, + } + } +} + +impl TryFrom for File { + type Error = core::Error; + + fn try_from(placeholder: Placeholder) -> core::Result { + match placeholder.handle.handle_type { + PlaceholderHandleType::Win32 => { + let file = + unsafe { File::from_raw_handle(mem::transmute(placeholder.handle.handle)) }; + Ok(file) + } + PlaceholderHandleType::CfApi => unsafe { + CfRevertPlaceholder( + placeholder.handle.handle, + CloudFilters::CF_REVERT_FLAG_NONE, + ptr::null_mut(), + ) + } + .map(|_| unsafe { File::from_raw_handle(mem::transmute(placeholder.handle.handle)) }), + } } } diff --git a/src/placeholder_file.rs b/src/placeholder_file.rs index ebbec02..0ba009e 100644 --- a/src/placeholder_file.rs +++ b/src/placeholder_file.rs @@ -50,7 +50,7 @@ impl PlaceholderFile { self } - /// Marks the [PlaceholderFile][crate::PlaceholderFile] as synced. + /// Marks the [PlaceholderFile][crate::PlaceholderFile] as in sync. /// /// This flag is used to determine the status of a placeholder shown in the file explorer. It /// is applicable to both files and directories. @@ -59,7 +59,7 @@ impl PlaceholderFile { /// A file that is partially full could still be marked as synced, any remaining data will /// invoke the [SyncFilter::fetch_data][crate::SyncFilter::fetch_data] callback automatically /// if requested. - pub fn mark_sync(mut self) -> Self { + pub fn mark_in_sync(mut self) -> Self { self.0.Flags |= CloudFilters::CF_PLACEHOLDER_CREATE_FLAG_MARK_IN_SYNC; self } @@ -110,6 +110,10 @@ impl PlaceholderFile { self } + pub fn get_usn(&self) -> Option { + self.0.CreateUsn.try_into().ok() + } + /// Creates a placeholder file/directory on the file system. /// /// The value returned is the final [Usn][crate::Usn] after the placeholder is created. @@ -183,7 +187,7 @@ impl BatchCreate for [PlaceholderFile] { } /// The metadata for a [PlaceholderFile][crate::PlaceholderFile]. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Default)] pub struct Metadata(pub(crate) CF_FS_METADATA); impl Metadata { diff --git a/src/request.rs b/src/request.rs index 9433195..43700c5 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,10 +1,8 @@ use std::{path::PathBuf, slice}; -use widestring::{U16CStr, U16CString}; +use widestring::{u16cstr, U16CStr, U16CString}; use windows::Win32::Storage::CloudFilters::{CF_CALLBACK_INFO, CF_PROCESS_INFO}; -use crate::placeholder::Placeholder; - pub type RawConnectionKey = isize; pub type RawTransferKey = i64; @@ -75,7 +73,6 @@ impl Request { self.0.FileSize as u64 } - // TODO: Create a U16Path struct to avoid an extra allocation // For now this should be cached on creation /// The absolute path of the placeholder file/directory starting from the root directory of the /// volume. @@ -105,7 +102,6 @@ impl Request { // self.0.RequestKey // } - // TODO: move file blob and file-related stuff to the placeholder struct? /// The byte slice assigned to the current placeholder file/directory. pub fn file_blob(&self) -> &[u8] { unsafe { @@ -126,26 +122,15 @@ impl Request { } } - /// Creates a new [Placeholder][crate::Placeholder] struct to perform various operations on the - /// current placeholder file/directory. - pub fn placeholder(&self) -> Placeholder { - Placeholder::new( - self.connection_key(), - self.transfer_key(), - self.path(), - self.file_size(), - ) - } - - // https://docs.microsoft.com/en-us/windows/win32/api/cfapi/ne-cfapi-cf_callback_type#remarks - // after 60 seconds of no report, windows will cancel the request with an error, - // this function is a "hack" to avoid the timeout - // https://docs.microsoft.com/en-us/windows/win32/api/cfapi/nf-cfapi-cfexecute#remarks - // CfExecute will reset any timers as stated - /// By default, the operating system will invalidate the callback after 60 seconds of no - /// activity (meaning, no placeholder methods are invoked). If you are prone to this issue, - /// consider calling this method or call placeholder methods more frequently. - pub fn reset_timeout() {} + // // https://docs.microsoft.com/en-us/windows/win32/api/cfapi/ne-cfapi-cf_callback_type#remarks + // // after 60 seconds of no report, windows will cancel the request with an error, + // // this function is a "hack" to avoid the timeout + // // https://docs.microsoft.com/en-us/windows/win32/api/cfapi/nf-cfapi-cfexecute#remarks + // // CfExecute will reset any timers as stated + // /// By default, the operating system will invalidate the callback after 60 seconds of no + // /// activity (meaning, no placeholder methods are invoked). If you are prone to this issue, + // /// consider calling this method or call placeholder methods more frequently. + // pub fn reset_timeout() {} } /// Information about the calling process. @@ -187,7 +172,7 @@ impl Process { /// retrieve the path. pub fn path(&self) -> Option { let path = unsafe { U16CString::from_ptr_str(self.0.ImagePath.0) }; - if path == unsafe { U16CString::from_str_unchecked("UNKNOWN") } { + if path == u16cstr!("UNKNOWN") { None } else { Some(path.to_os_string().into()) From d9586e5fa35dc917f81c6916a44e9800cdb290bc Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Sun, 23 Jun 2024 21:12:02 +0800 Subject: [PATCH 04/34] chore: PlaceholderFile --- Cargo.toml | 1 + examples/sftp/src/main.rs | 30 +++++---- src/command/commands.rs | 7 +- src/lib.rs | 5 +- src/metadata.rs | 129 ++++++++++++++++++++++++++++++++++++ src/placeholder.rs | 7 +- src/placeholder_file.rs | 133 ++++++-------------------------------- 7 files changed, 176 insertions(+), 136 deletions(-) create mode 100644 src/metadata.rs diff --git a/Cargo.toml b/Cargo.toml index 0e76f9b..cb4a155 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] widestring = "1.0.2" +nt-time = "0.8.0" memoffset = "0.6.4" windows = { version = "0.33.0", features = [ "alloc", diff --git a/examples/sftp/src/main.rs b/examples/sftp/src/main.rs index 838e255..1f87e7b 100644 --- a/examples/sftp/src/main.rs +++ b/examples/sftp/src/main.rs @@ -14,8 +14,10 @@ use thiserror::Error; use widestring::{u16str, U16String}; use wincs::{ filter::{info, ticket, SyncFilter}, + metadata::Metadata, + nt_time::FileTime, placeholder::ConvertOptions, - placeholder_file::{Metadata, PlaceholderFile}, + placeholder_file::PlaceholderFile, request::Request, CloudErrorKind, HydrationType, Placeholder, PopulationType, Registration, SecurityId, SyncRootIdBuilder, WriteAt, @@ -168,9 +170,9 @@ impl SyncFilter for Filter { .read(&mut buffer) .map_err(|_| CloudErrorKind::InvalidRequest)?; - if bytes_read % 4096 != 0 && position + (bytes_read as u64) < end { - let unaligned = bytes_read % 4096; - bytes_read = bytes_read - unaligned; + let unaligned = bytes_read % 4096; + if unaligned != 0 && position + (bytes_read as u64) < end { + bytes_read -= unaligned; server_file .seek(SeekFrom::Current(-(unaligned as i64))) .unwrap(); @@ -239,8 +241,8 @@ impl SyncFilter for Filter { (true, true) => { self.sftp .rename( - &src.strip_prefix(&base).unwrap(), - &dest.strip_prefix(&base).unwrap(), + src.strip_prefix(&base).unwrap(), + dest.strip_prefix(&base).unwrap(), None, ) .map_err(|_| CloudErrorKind::InvalidRequest)?; @@ -285,16 +287,16 @@ impl SyncFilter for Filter { let relative_path = path.strip_prefix(parent).unwrap(); PlaceholderFile::new(relative_path) .metadata( - if stat.is_dir() { - Metadata::directory() - } else { - Metadata::file() + match stat.is_dir() { + true => Metadata::directory(), + false => Metadata::file(), } .size(stat.size.unwrap_or_default()) - // .creation_time() // either the access time or write time, whichever is less - .last_access_time(stat.atime.unwrap_or_default()) - .last_write_time(stat.mtime.unwrap_or_default()) - .change_time(stat.mtime.unwrap_or_default()), + .accessed( + stat.atime + .and_then(|t| FileTime::from_unix_time(t as _).ok()) + .unwrap_or_default(), + ), ) .mark_in_sync() .overwrite() diff --git a/src/command/commands.rs b/src/command/commands.rs index 0cac37d..e7b3d23 100644 --- a/src/command/commands.rs +++ b/src/command/commands.rs @@ -16,7 +16,8 @@ use windows::{ use crate::{ command::executor::{execute, Command, Fallible}, error::CloudErrorKind, - placeholder_file::{Metadata, PlaceholderFile}, + metadata::Metadata, + placeholder_file::PlaceholderFile, request::{RawConnectionKey, RawTransferKey}, usn::Usn, }; @@ -112,7 +113,7 @@ impl Fallible for Write<'_> { #[derive(Debug)] pub struct Update<'a> { /// Whether or not to mark the placeholder as "synced." - pub mark_sync: bool, + pub mark_in_sync: bool, /// Optional metadata to update. pub metadata: Option, /// Optional file blob to update. @@ -130,7 +131,7 @@ impl Command for Update<'_> { fn build(&self) -> CF_OPERATION_PARAMETERS_0 { CF_OPERATION_PARAMETERS_0 { RestartHydration: CF_OPERATION_PARAMETERS_0_4 { - Flags: if self.mark_sync { + Flags: if self.mark_in_sync { CloudFilters::CF_OPERATION_RESTART_HYDRATION_FLAG_MARK_IN_SYNC } else { CloudFilters::CF_OPERATION_RESTART_HYDRATION_FLAG_NONE diff --git a/src/lib.rs b/src/lib.rs index a5bb0b6..48e0b6c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ mod error; /// Contains traits extending common structs from the [std][std]. pub mod ext; pub mod filter; +pub mod metadata; pub mod placeholder; pub mod placeholder_file; pub mod request; @@ -18,7 +19,7 @@ pub mod utility; pub use error::CloudErrorKind; pub use filter::{info, ticket, SyncFilter}; pub use placeholder::Placeholder; -pub use placeholder_file::{BatchCreate, Metadata, PlaceholderFile}; +pub use placeholder_file::{BatchCreate, PlaceholderFile}; pub use request::{Process, Request}; pub use root::{ active_roots, is_supported, Connection, HydrationPolicy, HydrationType, PopulationType, @@ -28,6 +29,8 @@ pub use root::{ pub use usn::Usn; pub use utility::{ReadAt, WriteAt}; +pub use nt_time; + mod sealed { pub trait Sealed {} } diff --git a/src/metadata.rs b/src/metadata.rs new file mode 100644 index 0000000..5f6bb0e --- /dev/null +++ b/src/metadata.rs @@ -0,0 +1,129 @@ +use std::fs; + +use nt_time::FileTime; +use windows::Win32::Storage::{ + CloudFilters::CF_FS_METADATA, + FileSystem::{FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_NORMAL, FILE_BASIC_INFO}, +}; + +use crate::sealed; + +/// The metadata for a [PlaceholderFile][crate::PlaceholderFile]. +#[derive(Debug, Clone, Copy, Default)] +pub struct Metadata(pub(crate) CF_FS_METADATA); + +impl Metadata { + pub fn file() -> Self { + Self(CF_FS_METADATA { + BasicInfo: FILE_BASIC_INFO { + FileAttributes: FILE_ATTRIBUTE_NORMAL.0, + ..Default::default() + }, + ..Default::default() + }) + } + + pub fn directory() -> Self { + Self(CF_FS_METADATA { + BasicInfo: FILE_BASIC_INFO { + FileAttributes: FILE_ATTRIBUTE_DIRECTORY.0, + ..Default::default() + }, + ..Default::default() + }) + } + + /// The time the file/directory was created. + pub fn created(mut self, time: FileTime) -> Self { + self.0.BasicInfo.CreationTime = time.try_into().unwrap(); + self + } + + /// The time the file/directory was last accessed. + pub fn accessed(mut self, time: FileTime) -> Self { + self.0.BasicInfo.LastAccessTime = time.try_into().unwrap(); + self + } + + /// The time the file/directory content was last written. + pub fn written(mut self, time: FileTime) -> Self { + self.0.BasicInfo.LastWriteTime = time.try_into().unwrap(); + self + } + + /// The time the file/directory content or metadata was changed. + pub fn changed(mut self, time: FileTime) -> Self { + self.0.BasicInfo.ChangeTime = time.try_into().unwrap(); + self + } + + /// The size of the file's content. + pub fn size(mut self, size: u64) -> Self { + self.0.FileSize = size as i64; + self + } + + /// File attributes. + pub fn attributes(mut self, attributes: u32) -> Self { + self.0.BasicInfo.FileAttributes |= attributes; + self + } +} + +pub trait MetadataExt: sealed::Sealed { + /// The time the file was changed in + /// [FILETIME](https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-filetime) format. + fn change_time(self, time: i64) -> Self; + + /// The time the file was last accessed in + /// [FILETIME](https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-filetime) format. + fn last_access_time(self, time: i64) -> Self; + + /// The time the file was last written to in + /// [FILETIME](https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-filetime) format. + fn last_write_time(self, time: i64) -> Self; + + /// The time the file was created in + /// [FILETIME](https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-filetime) format. + fn creation_time(self, time: i64) -> Self; +} + +impl MetadataExt for Metadata { + fn change_time(mut self, time: i64) -> Self { + self.0.BasicInfo.ChangeTime = time; + self + } + + fn last_access_time(mut self, time: i64) -> Self { + self.0.BasicInfo.LastAccessTime = time; + self + } + + fn last_write_time(mut self, time: i64) -> Self { + self.0.BasicInfo.LastWriteTime = time; + self + } + + fn creation_time(mut self, time: i64) -> Self { + self.0.BasicInfo.CreationTime = time; + self + } +} + +impl sealed::Sealed for Metadata {} + +impl From for Metadata { + fn from(metadata: fs::Metadata) -> Self { + use std::os::windows::fs::MetadataExt; + Self(CF_FS_METADATA { + BasicInfo: FILE_BASIC_INFO { + CreationTime: metadata.creation_time() as i64, + LastAccessTime: metadata.last_access_time() as i64, + LastWriteTime: metadata.last_write_time() as i64, + ChangeTime: metadata.last_write_time() as i64, + FileAttributes: metadata.file_attributes(), + }, + FileSize: metadata.file_size() as i64, + }) + } +} diff --git a/src/placeholder.rs b/src/placeholder.rs index 93dac94..3d5fb71 100644 --- a/src/placeholder.rs +++ b/src/placeholder.rs @@ -21,7 +21,7 @@ use windows::{ }, }; -use crate::{Metadata, Usn}; +use crate::{metadata::Metadata, Usn}; /// The type of handle that the placeholder file/directory owns. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -422,13 +422,13 @@ impl<'a> UpdateOptions<'a> { } /// Disables on-demand population for directories. - pub fn no_more_children(mut self) -> Self { + pub fn has_no_children(mut self) -> Self { self.flags |= CloudFilters::CF_UPDATE_FLAG_DISABLE_ON_DEMAND_POPULATION; self } /// Enable on-demand population for directories. - pub fn has_more_children(mut self) -> Self { + pub fn has_children(mut self) -> Self { self.flags |= CloudFilters::CF_UPDATE_FLAG_ENABLE_ON_DEMAND_POPULATION; self } @@ -632,6 +632,7 @@ impl From for Placeholder { impl TryFrom for File { type Error = core::Error; + #[allow(clippy::missing_transmute_annotations)] fn try_from(placeholder: Placeholder) -> core::Result { match placeholder.handle.handle_type { PlaceholderHandleType::Win32 => { diff --git a/src/placeholder_file.rs b/src/placeholder_file.rs index 0ba009e..f97527a 100644 --- a/src/placeholder_file.rs +++ b/src/placeholder_file.rs @@ -1,24 +1,17 @@ -use std::{fs, os::windows::prelude::MetadataExt, path::Path, ptr, slice}; +use std::{path::Path, ptr, slice}; use widestring::U16CString; use windows::{ core::{self, PCWSTR}, Win32::{ Foundation, - Storage::{ - CloudFilters::{ - self, CfCreatePlaceholders, CF_FS_METADATA, CF_PLACEHOLDER_CREATE_INFO, - }, - FileSystem::{FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_NORMAL, FILE_BASIC_INFO}, - }, + Storage::CloudFilters::{self, CfCreatePlaceholders, CF_PLACEHOLDER_CREATE_INFO}, }, }; -use crate::usn::Usn; +use crate::{metadata::Metadata, sealed, usn::Usn}; -// TODO: this struct could probably have a better name to represent files/dirs /// A builder for creating new placeholder files/directories. -#[repr(C)] #[derive(Debug)] pub struct PlaceholderFile(CF_PLACEHOLDER_CREATE_INFO); @@ -32,7 +25,7 @@ impl PlaceholderFile { .into_raw(), ), Flags: CloudFilters::CF_PLACEHOLDER_CREATE_FLAG_NONE, - Result: Foundation::S_OK, + Result: Foundation::S_FALSE, ..Default::default() }) } @@ -50,15 +43,11 @@ impl PlaceholderFile { self } - /// Marks the [PlaceholderFile][crate::PlaceholderFile] as in sync. + /// Marks a placeholder as in sync. /// - /// This flag is used to determine the status of a placeholder shown in the file explorer. It - /// is applicable to both files and directories. - /// - /// A file or directory should be marked as "synced" when it has all of its data and metadata. - /// A file that is partially full could still be marked as synced, any remaining data will - /// invoke the [SyncFilter::fetch_data][crate::SyncFilter::fetch_data] callback automatically - /// if requested. + /// See also + /// [SetInSyncState](https://learn.microsoft.com/en-us/windows/win32/api/cfapi/nf-cfapi-cfsetinsyncstate), + /// [What does "In-Sync" Mean?](https://www.userfilesystem.com/programming/faq/#nav_whatdoesin-syncmean) pub fn mark_in_sync(mut self) -> Self { self.0.Flags |= CloudFilters::CF_PLACEHOLDER_CREATE_FLAG_MARK_IN_SYNC; self @@ -99,19 +88,20 @@ impl PlaceholderFile { ); if blob.is_empty() { + self.0.FileIdentity = ptr::null(); + self.0.FileIdentityLength = 0; return self; } let leaked_blob = Box::leak(blob.into_boxed_slice()); - self.0.FileIdentity = leaked_blob.as_ptr() as *const _; self.0.FileIdentityLength = leaked_blob.len() as _; self } - pub fn get_usn(&self) -> Option { - self.0.CreateUsn.try_into().ok() + pub fn result(&self) -> core::Result { + self.0.Result.ok().map(|_| self.0.CreateUsn as _) } /// Creates a placeholder file/directory on the file system. @@ -135,7 +125,7 @@ impl PlaceholderFile { )?; } - self.0.Result.ok().map(|_| self.0.CreateUsn as Usn) + self.result() } } @@ -157,12 +147,12 @@ impl Drop for PlaceholderFile { } /// Creates multiple placeholder file/directories within the given path. -pub trait BatchCreate { - fn create>(&mut self, path: P) -> core::Result>>; +pub trait BatchCreate: sealed::Sealed { + fn create>(&mut self, path: P) -> core::Result<()>; } impl BatchCreate for [PlaceholderFile] { - fn create>(&mut self, path: P) -> core::Result>> { + fn create>(&mut self, path: P) -> core::Result<()> { unsafe { CfCreatePlaceholders( path.as_ref().as_os_str(), @@ -170,96 +160,9 @@ impl BatchCreate for [PlaceholderFile] { self.len() as u32, CloudFilters::CF_CREATE_FLAG_NONE, ptr::null_mut(), - )?; + ) } - - Ok(self - .iter() - .map(|placeholder| { - placeholder - .0 - .Result - .ok() - .map(|_| placeholder.0.CreateUsn as Usn) - }) - .collect()) } } -/// The metadata for a [PlaceholderFile][crate::PlaceholderFile]. -#[derive(Debug, Clone, Copy, Default)] -pub struct Metadata(pub(crate) CF_FS_METADATA); - -impl Metadata { - pub fn file() -> Self { - Self(CF_FS_METADATA { - BasicInfo: FILE_BASIC_INFO { - FileAttributes: FILE_ATTRIBUTE_NORMAL.0, - ..Default::default() - }, - ..Default::default() - }) - } - - pub fn directory() -> Self { - Self(CF_FS_METADATA { - BasicInfo: FILE_BASIC_INFO { - FileAttributes: FILE_ATTRIBUTE_DIRECTORY.0, - ..Default::default() - }, - ..Default::default() - }) - } - - /// The time the file/directory was created. - pub fn creation_time(mut self, time: u64) -> Self { - self.0.BasicInfo.CreationTime = time as i64; - self - } - - /// The time the file/directory was last accessed. - pub fn last_access_time(mut self, time: u64) -> Self { - self.0.BasicInfo.LastAccessTime = time as i64; - self - } - - /// The time the file/directory content was last written. - pub fn last_write_time(mut self, time: u64) -> Self { - self.0.BasicInfo.LastWriteTime = time as i64; - self - } - - /// The time the file/directory content or metadata was changed. - pub fn change_time(mut self, time: u64) -> Self { - self.0.BasicInfo.ChangeTime = time as i64; - self - } - - /// The size of the file's content. - pub fn size(mut self, size: u64) -> Self { - self.0.FileSize = size as i64; - self - } - - // TODO: create a method for specifying that it's a directory. - /// File attributes. - pub fn attributes(mut self, attributes: u32) -> Self { - self.0.BasicInfo.FileAttributes |= attributes; - self - } -} - -impl From for Metadata { - fn from(metadata: fs::Metadata) -> Self { - Self(CF_FS_METADATA { - BasicInfo: FILE_BASIC_INFO { - CreationTime: metadata.creation_time() as i64, - LastAccessTime: metadata.last_access_time() as i64, - LastWriteTime: metadata.last_write_time() as i64, - ChangeTime: metadata.last_write_time() as i64, - FileAttributes: metadata.file_attributes(), - }, - FileSize: metadata.file_size() as i64, - }) - } -} +impl sealed::Sealed for [PlaceholderFile] {} From c6a12603cc8507e688b687e81a9319ec66f04858 Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Mon, 24 Jun 2024 19:24:25 +0800 Subject: [PATCH 05/34] chore: export modules --- examples/sftp/src/main.rs | 14 +++++++------- src/filter/ticket.rs | 5 ++++- src/lib.rs | 17 +---------------- src/placeholder.rs | 2 +- src/root/register.rs | 4 +++- src/utility.rs | 2 ++ 6 files changed, 18 insertions(+), 26 deletions(-) diff --git a/examples/sftp/src/main.rs b/examples/sftp/src/main.rs index 1f87e7b..e515872 100644 --- a/examples/sftp/src/main.rs +++ b/examples/sftp/src/main.rs @@ -9,18 +9,18 @@ use std::{ }; use rkyv::{Archive, Deserialize, Serialize}; -use ssh2::{Session, Sftp}; +use ssh2::Sftp; use thiserror::Error; use widestring::{u16str, U16String}; use wincs::{ + error::CloudErrorKind, filter::{info, ticket, SyncFilter}, metadata::Metadata, - nt_time::FileTime, - placeholder::ConvertOptions, + placeholder::{ConvertOptions, Placeholder}, placeholder_file::PlaceholderFile, request::Request, - CloudErrorKind, HydrationType, Placeholder, PopulationType, Registration, SecurityId, - SyncRootIdBuilder, WriteAt, + root::{HydrationType, PopulationType, Registration, SecurityId, Session, SyncRootIdBuilder}, + utility::{FileTime, WriteAt}, }; // max should be 65536, this is done both in term-scp and sshfs because it's the @@ -38,7 +38,7 @@ pub struct FileBlob { fn main() { let tcp = TcpStream::connect(env::var("SERVER").unwrap()).unwrap(); - let mut session = Session::new().unwrap(); + let mut session = ssh2::Session::new().unwrap(); session.set_blocking(true); session.set_tcp_stream(tcp); session.handshake().unwrap(); @@ -74,7 +74,7 @@ fn main() { mark_in_sync(Path::new(&client_path), &sftp); - let connection = wincs::Session::new() + let connection = Session::new() .connect(&client_path, Filter { sftp }) .unwrap(); diff --git a/src/filter/ticket.rs b/src/filter/ticket.rs index e356662..9375c67 100644 --- a/src/filter/ticket.rs +++ b/src/filter/ticket.rs @@ -8,8 +8,11 @@ use windows::{ use crate::{ command::{self, Command, Fallible}, error::CloudErrorKind, + placeholder_file::PlaceholderFile, request::{RawConnectionKey, RawTransferKey}, - sealed, utility, PlaceholderFile, Usn, + sealed, + usn::Usn, + utility, }; /// A ticket for the [SyncFilter::fetch_data][crate::SyncFilter::fetch_data] callback. diff --git a/src/lib.rs b/src/lib.rs index 48e0b6c..b0f77f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ /// [Request][crate::Request] and [Placeholder][crate::Placeholder]. Thus, it is not necessary to /// create and call these structs manually unless you need more granular access. pub mod command; -mod error; +pub mod error; /// Contains traits extending common structs from the [std][std]. pub mod ext; pub mod filter; @@ -16,21 +16,6 @@ pub mod root; pub mod usn; pub mod utility; -pub use error::CloudErrorKind; -pub use filter::{info, ticket, SyncFilter}; -pub use placeholder::Placeholder; -pub use placeholder_file::{BatchCreate, PlaceholderFile}; -pub use request::{Process, Request}; -pub use root::{ - active_roots, is_supported, Connection, HydrationPolicy, HydrationType, PopulationType, - ProtectionMode, Registration, SecurityId, Session, SupportedAttributes, SyncRootId, - SyncRootIdBuilder, -}; -pub use usn::Usn; -pub use utility::{ReadAt, WriteAt}; - -pub use nt_time; - mod sealed { pub trait Sealed {} } diff --git a/src/placeholder.rs b/src/placeholder.rs index 3d5fb71..985c30e 100644 --- a/src/placeholder.rs +++ b/src/placeholder.rs @@ -21,7 +21,7 @@ use windows::{ }, }; -use crate::{metadata::Metadata, Usn}; +use crate::{metadata::Metadata, usn::Usn}; /// The type of handle that the placeholder file/directory owns. #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/src/root/register.rs b/src/root/register.rs index 548fa53..e91cf5a 100644 --- a/src/root/register.rs +++ b/src/root/register.rs @@ -21,7 +21,9 @@ use windows::{ }, }; -use crate::{utility::ToHString, SyncRootId}; +use crate::utility::ToHString; + +use super::SyncRootId; #[derive(Debug, Clone)] pub struct Registration<'a> { diff --git a/src/utility.rs b/src/utility.rs index eb14958..8dfcaeb 100644 --- a/src/utility.rs +++ b/src/utility.rs @@ -2,6 +2,8 @@ use windows::core::{self, HSTRING}; use crate::sealed; +pub use nt_time::FileTime; + // TODO: add something to convert an Option to a *const T and *mut T pub(crate) trait ToHString From 7678aa73604c6beb13c3b29bbf6c836021ba2155 Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Mon, 24 Jun 2024 21:24:38 +0800 Subject: [PATCH 06/34] chore: filter info --- src/filter/info.rs | 182 +++++++++++++++++++++++++++++--------------- src/filter/proxy.rs | 8 +- 2 files changed, 127 insertions(+), 63 deletions(-) diff --git a/src/filter/info.rs b/src/filter/info.rs index c9f6740..2f77cba 100644 --- a/src/filter/info.rs +++ b/src/filter/info.rs @@ -1,16 +1,16 @@ use std::{ffi::OsString, fmt::Debug, ops::Range, path::PathBuf}; +use nt_time::FileTime; use widestring::U16CStr; use windows::Win32::Storage::CloudFilters::{ - self, CF_CALLBACK_CANCEL_FLAGS, CF_CALLBACK_DEHYDRATION_REASON, CF_CALLBACK_PARAMETERS_0_0, - CF_CALLBACK_PARAMETERS_0_1, CF_CALLBACK_PARAMETERS_0_10, CF_CALLBACK_PARAMETERS_0_11, - CF_CALLBACK_PARAMETERS_0_2, CF_CALLBACK_PARAMETERS_0_3, CF_CALLBACK_PARAMETERS_0_4, - CF_CALLBACK_PARAMETERS_0_5, CF_CALLBACK_PARAMETERS_0_6, CF_CALLBACK_PARAMETERS_0_7, - CF_CALLBACK_PARAMETERS_0_8, CF_CALLBACK_PARAMETERS_0_9, + self, CF_CALLBACK_DEHYDRATION_REASON, CF_CALLBACK_PARAMETERS_0_0, CF_CALLBACK_PARAMETERS_0_1, + CF_CALLBACK_PARAMETERS_0_10, CF_CALLBACK_PARAMETERS_0_11, CF_CALLBACK_PARAMETERS_0_2, + CF_CALLBACK_PARAMETERS_0_3, CF_CALLBACK_PARAMETERS_0_4, CF_CALLBACK_PARAMETERS_0_5, + CF_CALLBACK_PARAMETERS_0_6, CF_CALLBACK_PARAMETERS_0_7, CF_CALLBACK_PARAMETERS_0_8, + CF_CALLBACK_PARAMETERS_0_9, }; /// Information for the [SyncFilter::fetch_data][crate::SyncFilter::fetch_data] callback. -#[derive(Debug, Clone, Copy)] pub struct FetchData(pub(crate) CF_CALLBACK_PARAMETERS_0_6); impl FetchData { @@ -25,25 +25,25 @@ impl FetchData { (self.0.Flags & CloudFilters::CF_CALLBACK_FETCH_DATA_FLAG_EXPLICIT_HYDRATION).0 != 0 } - // TODO: does this amount always lay on 4kb or EoF? /// The amount of bytes that must be written to the placeholder. pub fn required_file_range(&self) -> Range { (self.0.RequiredFileOffset as u64) ..(self.0.RequiredFileOffset + self.0.RequiredLength) as u64 } - // TODO: what is this field - // https://docs.microsoft.com/en-us/answers/questions/748214/what-is-fetchdataoptionalfileoffset-cfapi.html + /// The amount of bytes that must be written to the placeholder. + /// + /// If the sync provider prefer to give data in larger chunks, use this range instead. + /// + /// [Discussion](https://docs.microsoft.com/en-us/answers/questions/748214/what-is-fetchdataoptionalfileoffset-cfapi.html). pub fn optional_file_range(&self) -> Range { (self.0.OptionalFileOffset as u64) ..(self.0.OptionalFileOffset + self.0.OptionalLength) as u64 } /// The last time the file was dehydrated. - /// - /// This value is a count of 100-nanosecond intervals since January 1, 1601. - pub fn last_dehydration_time(&self) -> u64 { - self.0.LastDehydrationTime as u64 + pub fn last_dehydration_time(&self) -> FileTime { + self.0.LastDehydrationTime.try_into().unwrap() } /// The reason the file was last dehydrated. @@ -52,8 +52,19 @@ impl FetchData { } } +impl Debug for FetchData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FetchData") + .field("interrupted_hydration", &self.interrupted_hydration()) + .field("required_file_range", &self.required_file_range()) + .field("optional_file_range", &self.optional_file_range()) + .field("last_dehydration_time", &self.last_dehydration_time()) + .field("last_dehydration_reason", &self.last_dehydration_reason()) + .finish() + } +} + /// Information for the [SyncFilter::cancel_fetch_data][crate::SyncFilter::cancel_fetch_data] callback. -#[derive(Clone, Copy)] pub struct CancelFetchData(pub(crate) CF_CALLBACK_PARAMETERS_0_0); impl CancelFetchData { @@ -71,7 +82,7 @@ impl CancelFetchData { (self.0.Flags & CloudFilters::CF_CALLBACK_CANCEL_FLAG_IO_ABORTED).0 != 0 } - /// The range of data that was supposed to be fetched. + /// The range of the file data that is no longer required. pub fn file_range(&self) -> Range { let range = unsafe { self.0.Anonymous.FetchData }; (range.FileOffset as u64)..(range.FileOffset + range.Length) as u64 @@ -80,28 +91,15 @@ impl CancelFetchData { impl Debug for CancelFetchData { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("CancelFetchData") - .field(unsafe { - &CancelFetchDataDebug { - Flags: self.0.Flags, - FileOffset: self.0.Anonymous.FetchData.FileOffset, - Length: self.0.Anonymous.FetchData.Length, - } - }) + f.debug_struct("CancelFetchData") + .field("timeout", &self.timeout()) + .field("user_cancelled", &self.user_cancelled()) + .field("file_range", &self.file_range()) .finish() } } -#[allow(dead_code, non_snake_case)] -#[derive(Debug)] -struct CancelFetchDataDebug { - Flags: CF_CALLBACK_CANCEL_FLAGS, - FileOffset: i64, - Length: i64, -} - /// Information for the [SyncFilter::validate_data][crate::SyncFilter::validate_data] callback. -#[derive(Debug, Clone, Copy)] pub struct ValidateData(pub(crate) CF_CALLBACK_PARAMETERS_0_11); impl ValidateData { @@ -117,9 +115,17 @@ impl ValidateData { } } +impl Debug for ValidateData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ValidateData") + .field("explicit_hydration", &self.explicit_hydration()) + .field("file_range", &self.file_range()) + .finish() + } +} + /// Information for the [SyncFilter::fetch_placeholders][crate::SyncFilter::fetch_placeholders] /// callback. -#[derive(Debug)] pub struct FetchPlaceholders(pub(crate) CF_CALLBACK_PARAMETERS_0_7); impl FetchPlaceholders { @@ -136,14 +142,21 @@ impl FetchPlaceholders { /// /// This field is completely optional and does not have to be respected. #[cfg(not(feature = "globs"))] - pub fn pattern(&self) -> &U16CStr { - unsafe { U16CStr::from_ptr_str(self.0.Pattern.0) } + pub fn pattern(&self) -> String { + unsafe { U16CStr::from_ptr_str(self.0.Pattern.0) }.to_string_lossy() + } +} + +impl Debug for FetchPlaceholders { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FetchPlaceholders") + .field("pattern", &self.pattern()) + .finish() } } /// Information for the /// [SyncFilter::cancel_fetch_placeholders][crate::SyncFilter::cancel_fetch_placeholders] callback. -#[derive(Clone, Copy)] pub struct CancelFetchPlaceholders(pub(crate) CF_CALLBACK_PARAMETERS_0_0); impl CancelFetchPlaceholders { @@ -164,22 +177,14 @@ impl CancelFetchPlaceholders { impl Debug for CancelFetchPlaceholders { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("CancelFetchPlaceholders") - .field(&CancelFetchPlaceholdersDebug { - Flags: self.0.Flags, - }) + f.debug_struct("CancelFetchPlaceholders") + .field("timeout", &self.timeout()) + .field("user_cancelled", &self.user_cancelled()) .finish() } } -#[allow(dead_code, non_snake_case)] -#[derive(Debug)] -struct CancelFetchPlaceholdersDebug { - Flags: CF_CALLBACK_CANCEL_FLAGS, -} - /// Information for the [SyncFilter::opened][crate::SyncFilter::opened] callback. -#[derive(Debug, Clone, Copy)] pub struct Opened(pub(crate) CF_CALLBACK_PARAMETERS_0_8); impl Opened { @@ -195,8 +200,16 @@ impl Opened { } } +impl Debug for Opened { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Opened") + .field("metadata_corrupt", &self.metadata_corrupt()) + .field("metadata_unsupported", &self.metadata_unsupported()) + .finish() + } +} + /// Information for the [SyncFilter::closed][crate::SyncFilter::closed] callback. -#[derive(Debug, Clone, Copy)] pub struct Closed(pub(crate) CF_CALLBACK_PARAMETERS_0_1); impl Closed { @@ -206,8 +219,15 @@ impl Closed { } } +impl Debug for Closed { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Closed") + .field("deleted", &self.deleted()) + .finish() + } +} + /// Information for the [SyncFilter::dehydrate][crate::SyncFilter::dehydrate] callback. -#[derive(Debug, Clone, Copy)] pub struct Dehydrate(pub(crate) CF_CALLBACK_PARAMETERS_0_3); impl Dehydrate { @@ -222,8 +242,16 @@ impl Dehydrate { } } +impl Debug for Dehydrate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Dehydrate") + .field("background", &self.background()) + .field("reason", &self.reason()) + .finish() + } +} + /// Information for the [SyncFilter::dehydrated][crate::SyncFilter::dehydrated] callback. -#[derive(Debug, Clone, Copy)] pub struct Dehydrated(pub(crate) CF_CALLBACK_PARAMETERS_0_2); impl Dehydrated { @@ -243,8 +271,17 @@ impl Dehydrated { } } +impl Debug for Dehydrated { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Dehydrated") + .field("background", &self.background()) + .field("already_hydrated", &self.already_hydrated()) + .field("reason", &self.reason()) + .finish() + } +} + /// Information for the [SyncFilter::delete][crate::SyncFilter::delete] callback. -#[derive(Debug, Clone, Copy)] pub struct Delete(pub(crate) CF_CALLBACK_PARAMETERS_0_5); impl Delete { @@ -254,18 +291,27 @@ impl Delete { } // TODO: missing docs + /// The placeholder is being undeleted. pub fn is_undelete(&self) -> bool { (self.0.Flags & CloudFilters::CF_CALLBACK_DELETE_FLAG_IS_UNDELETE).0 != 0 } } +impl Debug for Delete { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Delete") + .field("is_directory", &self.is_directory()) + .field("is_undelete", &self.is_undelete()) + .finish() + } +} + /// Information for the [SyncFilter::deleted][crate::SyncFilter::deleted] callback. -#[derive(Debug, Clone, Copy)] +#[derive(Debug)] #[allow(dead_code)] pub struct Deleted(pub(crate) CF_CALLBACK_PARAMETERS_0_4); /// Information for the [SyncFilter::rename][crate::SyncFilter::rename] callback. -#[derive(Debug)] pub struct Rename(pub(crate) CF_CALLBACK_PARAMETERS_0_10, pub(crate) OsString); impl Rename { @@ -292,18 +338,34 @@ impl Rename { } } +impl Debug for Rename { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Rename") + .field("is_directory", &self.is_directory()) + .field("source_in_scope", &self.source_in_scope()) + .field("target_in_scope", &self.target_in_scope()) + .field("target_path", &self.target_path()) + .finish() + } +} + /// Information for the [SyncFilter::renamed][crate::SyncFilter::renamed] callback. -#[derive(Debug)] -pub struct Renamed(pub(crate) CF_CALLBACK_PARAMETERS_0_9); +pub struct Renamed(pub(crate) CF_CALLBACK_PARAMETERS_0_9, pub(crate) OsString); impl Renamed { /// The full path the placeholder has been moved from. pub fn source_path(&self) -> PathBuf { - unsafe { - U16CStr::from_ptr_str(self.0.SourcePath.0) - .to_os_string() - .into() - } + let mut path = PathBuf::from(&self.1); + path.push(unsafe { U16CStr::from_ptr_str(self.0.SourcePath.0) }.to_os_string()); + path + } +} + +impl Debug for Renamed { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Renamed") + .field("source_path", &self.source_path()) + .finish() } } diff --git a/src/filter/proxy.rs b/src/filter/proxy.rs index 49b19cb..03893e7 100644 --- a/src/filter/proxy.rs +++ b/src/filter/proxy.rs @@ -246,10 +246,12 @@ pub unsafe extern "system" fn notify_rename_completion( params: *const CF_CALLBACK_PARAMETERS, ) { if let Some(filter) = filter_from_info::(info) { - filter.renamed( - Request::new(*info), - info::Renamed((*params).Anonymous.RenameCompletion), + let request = Request::new(*info); + let info = info::Renamed( + (*params).Anonymous.RenameCompletion, + request.volume_letter().to_os_string(), ); + filter.renamed(request, info); } } From 35e91d7d1e0698d1942ebd99a685c62a5adca1cc Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Mon, 24 Jun 2024 21:50:45 +0800 Subject: [PATCH 07/34] chore: request --- src/command/mod.rs | 2 +- src/filter/mod.rs | 7 ++--- src/filter/proxy.rs | 7 ++--- src/lib.rs | 12 ++++----- src/request.rs | 63 ++++++++++++++++++++++++--------------------- 5 files changed, 46 insertions(+), 45 deletions(-) diff --git a/src/command/mod.rs b/src/command/mod.rs index 1edd8dd..cf15733 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -1,5 +1,5 @@ mod commands; mod executor; -pub use commands::{CreatePlaceholders, Dehydrate, Delete, Read, Rename, Update, Validate, Write}; +pub use commands::{CreatePlaceholders, Dehydrate, Delete, Read, Rename, Validate, Write}; pub use executor::{Command, Fallible}; diff --git a/src/filter/mod.rs b/src/filter/mod.rs index 47b7113..0e21055 100644 --- a/src/filter/mod.rs +++ b/src/filter/mod.rs @@ -1,9 +1,10 @@ /// Information for callbacks in the [SyncFilter][crate::SyncFilter] trait. pub mod info; -mod proxy; -mod sync_filter; /// Tickets for callbacks in the [SyncFilter][crate::SyncFilter] trait. pub mod ticket; -pub use proxy::{callbacks, Callbacks}; +pub(crate) use proxy::{callbacks, Callbacks}; pub use sync_filter::SyncFilter; + +mod proxy; +mod sync_filter; diff --git a/src/filter/proxy.rs b/src/filter/proxy.rs index 03893e7..08b1255 100644 --- a/src/filter/proxy.rs +++ b/src/filter/proxy.rs @@ -232,10 +232,7 @@ pub unsafe extern "system" fn notify_rename( if let Some(filter) = filter_from_info::(info) { let request = Request::new(*info); let ticket = ticket::Rename::new(request.connection_key(), request.transfer_key()); - let info = info::Rename( - (*params).Anonymous.Rename, - request.volume_letter().to_os_string(), - ); + let info = info::Rename((*params).Anonymous.Rename, request.volume_letter()); filter.rename(request, ticket, info); } @@ -249,7 +246,7 @@ pub unsafe extern "system" fn notify_rename_completion( let request = Request::new(*info); let info = info::Renamed( (*params).Anonymous.RenameCompletion, - request.volume_letter().to_os_string(), + request.volume_letter(), ); filter.renamed(request, info); } diff --git a/src/lib.rs b/src/lib.rs index b0f77f5..ca806b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,3 @@ -/// Contains low-level structs for directly executing Cloud Filter operations. -/// -/// The [command][crate::command] API is exposed through various higher-level structs, like -/// [Request][crate::Request] and [Placeholder][crate::Placeholder]. Thus, it is not necessary to -/// create and call these structs manually unless you need more granular access. -pub mod command; pub mod error; /// Contains traits extending common structs from the [std][std]. pub mod ext; @@ -16,6 +10,12 @@ pub mod root; pub mod usn; pub mod utility; +/// Contains low-level structs for directly executing Cloud Filter operations. +/// +/// The [command][crate::command] API is exposed through various higher-level structs, like +/// [Request][crate::request::Request] and [Placeholder][crate::placeholder::Placeholder]. +mod command; + mod sealed { pub trait Sealed {} } diff --git a/src/request.rs b/src/request.rs index 43700c5..59d55a6 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,6 +1,6 @@ -use std::{path::PathBuf, slice}; +use std::{ffi::OsString, path::PathBuf, slice}; -use widestring::{u16cstr, U16CStr, U16CString}; +use widestring::{u16cstr, U16CStr}; use windows::Win32::Storage::CloudFilters::{CF_CALLBACK_INFO, CF_PROCESS_INFO}; pub type RawConnectionKey = isize; @@ -21,28 +21,18 @@ impl Request { Self(info) } - /// A raw connection key used to identify the connection. - pub fn connection_key(&self) -> RawConnectionKey { - self.0.ConnectionKey.0 - } - - /// A raw transfer key used to identify the current file operation. - pub fn transfer_key(&self) -> RawTransferKey { - self.0.TransferKey - } - /// The GUID path of the current volume. /// /// The returned value comes in the form `\?\Volume{GUID}`. - pub fn volume_guid_path(&self) -> &U16CStr { - unsafe { U16CStr::from_ptr_str(self.0.VolumeGuidName.0) } + pub fn volume_guid_path(&self) -> OsString { + unsafe { U16CStr::from_ptr_str(self.0.VolumeGuidName.0) }.to_os_string() } /// The letter of the current volume. /// /// The returned value comes in the form `X:`, where `X` is the drive letter. - pub fn volume_letter(&self) -> &U16CStr { - unsafe { U16CStr::from_ptr_str(self.0.VolumeDosName.0) } + pub fn volume_letter(&self) -> OsString { + unsafe { U16CStr::from_ptr_str(self.0.VolumeDosName.0) }.to_os_string() } /// The serial number of the current volume. @@ -131,6 +121,16 @@ impl Request { // /// activity (meaning, no placeholder methods are invoked). If you are prone to this issue, // /// consider calling this method or call placeholder methods more frequently. // pub fn reset_timeout() {} + + /// A raw connection key used to identify the connection. + pub(crate) fn connection_key(&self) -> RawConnectionKey { + self.0.ConnectionKey.0 + } + + /// A raw transfer key used to identify the current file operation. + pub(crate) fn transfer_key(&self) -> RawTransferKey { + self.0.TransferKey + } } /// Information about the calling process. @@ -139,8 +139,8 @@ pub struct Process(CF_PROCESS_INFO); impl Process { /// The application's package name. - pub fn name(&self) -> &U16CStr { - unsafe { U16CStr::from_ptr_str(self.0.PackageName.0) } + pub fn name(&self) -> OsString { + unsafe { U16CStr::from_ptr_str(self.0.PackageName.0) }.to_os_string() } /// The ID of the user process. @@ -149,20 +149,27 @@ impl Process { } /// The ID of the session where the user process resides. + /// + /// ## Note + /// + /// [session_id][crate::request::Process::session_id] is valid in versions 1803 and later. pub fn session_id(&self) -> u32 { self.0.SessionId } /// The application's ID. - pub fn application_id(&self) -> &U16CStr { - unsafe { U16CStr::from_ptr_str(self.0.ApplicationId.0) } + pub fn application_id(&self) -> OsString { + unsafe { U16CStr::from_ptr_str(self.0.ApplicationId.0) }.to_os_string() } - // TODO: command_line and session_id are valid only in versions 1803+ - // https://docs.microsoft.com/en-us/windows/win32/api/cfapi/ns-cfapi-cf_process_infoessionid /// The exact command used to initialize the user process. - pub fn command_line(&self) -> &U16CStr { - unsafe { U16CStr::from_ptr_str(self.0.CommandLine.0) } + /// + /// ## Note + /// + /// [command_line][crate::request::Process::command_line] is valid in versions 1803 and later. + pub fn command_line(&self) -> Option { + let cmd = unsafe { U16CStr::from_ptr_str(self.0.ImagePath.0) }; + (cmd != u16cstr!("UNKNOWN")).then(|| cmd.to_os_string()) } // TODO: Could be optimized @@ -171,11 +178,7 @@ impl Process { /// This function returns [None][std::option::Option::None] when the operating system failed to /// retrieve the path. pub fn path(&self) -> Option { - let path = unsafe { U16CString::from_ptr_str(self.0.ImagePath.0) }; - if path == u16cstr!("UNKNOWN") { - None - } else { - Some(path.to_os_string().into()) - } + let path = unsafe { U16CStr::from_ptr_str(self.0.ImagePath.0) }; + (path != u16cstr!("UNKNOWN")).then(|| PathBuf::from(path.to_os_string())) } } From 5bfa6b3ff5c29b3820787764c93b37a33b0862de Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Tue, 25 Jun 2024 20:46:18 +0800 Subject: [PATCH 08/34] refactor: returns Result::Err to response error --- examples/sftp/src/main.rs | 192 ++++++++++++++++++-------------------- src/command/commands.rs | 26 +----- src/error.rs | 3 + src/filter/proxy.rs | 71 ++++++++++---- src/filter/sync_filter.rs | 85 +++++++++-------- src/filter/ticket.rs | 47 +--------- 6 files changed, 200 insertions(+), 224 deletions(-) diff --git a/examples/sftp/src/main.rs b/examples/sftp/src/main.rs index e515872..7ec3f55 100644 --- a/examples/sftp/src/main.rs +++ b/examples/sftp/src/main.rs @@ -13,7 +13,7 @@ use ssh2::Sftp; use thiserror::Error; use widestring::{u16str, U16String}; use wincs::{ - error::CloudErrorKind, + error::{CResult, CloudErrorKind}, filter::{info, ticket, SyncFilter}, metadata::Metadata, placeholder::{ConvertOptions, Placeholder}, @@ -140,7 +140,12 @@ impl Filter { } impl SyncFilter for Filter { - fn fetch_data(&self, request: Request, ticket: ticket::FetchData, info: info::FetchData) { + fn fetch_data( + &self, + request: Request, + ticket: ticket::FetchData, + info: info::FetchData, + ) -> CResult<()> { let path = Path::new(unsafe { OsStr::from_encoded_bytes_unchecked(request.file_blob()) }); let range = info.required_file_range(); @@ -153,112 +158,93 @@ impl SyncFilter for Filter { range, info.interrupted_hydration() ); - - let res = || -> Result<(), _> { - let mut server_file = self - .sftp - .open(path) + let mut server_file = self + .sftp + .open(path) + .map_err(|_| CloudErrorKind::InvalidRequest)?; + server_file + .seek(SeekFrom::Start(position)) + .map_err(|_| CloudErrorKind::InvalidRequest)?; + + let mut buffer = [0; DOWNLOAD_CHUNK_SIZE_BYTES]; + + loop { + let mut bytes_read = server_file + .read(&mut buffer) .map_err(|_| CloudErrorKind::InvalidRequest)?; - server_file - .seek(SeekFrom::Start(position)) - .map_err(|_| CloudErrorKind::InvalidRequest)?; - - let mut buffer = [0; DOWNLOAD_CHUNK_SIZE_BYTES]; - - loop { - let mut bytes_read = server_file - .read(&mut buffer) - .map_err(|_| CloudErrorKind::InvalidRequest)?; - let unaligned = bytes_read % 4096; - if unaligned != 0 && position + (bytes_read as u64) < end { - bytes_read -= unaligned; - server_file - .seek(SeekFrom::Current(-(unaligned as i64))) - .unwrap(); - } - ticket - .write_at(&buffer[0..bytes_read], position) - .map_err(|_| CloudErrorKind::InvalidRequest)?; - position += bytes_read as u64; - - if position >= end { - break; - } - - ticket.report_progress(end, position).unwrap(); + let unaligned = bytes_read % 4096; + if unaligned != 0 && position + (bytes_read as u64) < end { + bytes_read -= unaligned; + server_file + .seek(SeekFrom::Current(-(unaligned as i64))) + .unwrap(); } + ticket + .write_at(&buffer[0..bytes_read], position) + .map_err(|_| CloudErrorKind::InvalidRequest)?; + position += bytes_read as u64; - Ok(()) - }(); + if position >= end { + break; + } - if let Err(e) = res { - ticket.fail(e).unwrap(); + ticket.report_progress(end, position).unwrap(); } + + Ok(()) } fn deleted(&self, _request: Request, _info: info::Deleted) { println!("deleted"); } - fn delete(&self, request: Request, ticket: ticket::Delete, info: info::Delete) { + fn delete(&self, request: Request, ticket: ticket::Delete, info: info::Delete) -> CResult<()> { println!("delete {:?}", request.path()); let path = Path::new(unsafe { OsStr::from_encoded_bytes_unchecked(request.file_blob()) }); - let res = || -> Result<(), _> { - match info.is_directory() { - true => self - .remove_remote_dir_all(path) - .map_err(|_| CloudErrorKind::InvalidRequest)?, - false => self - .sftp - .unlink(path) - .map_err(|_| CloudErrorKind::InvalidRequest)?, - } - ticket.pass().unwrap(); - Ok(()) - }(); - - if let Err(e) = res { - ticket.fail(e).unwrap(); + match info.is_directory() { + true => self + .remove_remote_dir_all(path) + .map_err(|_| CloudErrorKind::InvalidRequest)?, + false => self + .sftp + .unlink(path) + .map_err(|_| CloudErrorKind::InvalidRequest)?, } + ticket.pass().unwrap(); + Ok(()) } - fn rename(&self, request: Request, ticket: ticket::Rename, info: info::Rename) { - let res = || -> Result<(), _> { - let src = request.path(); - let dest = info.target_path(); - let base = get_client_path(); - - println!( - "rename {} to {}, source in scope: {}, target in scope: {}", - src.display(), - dest.display(), - info.source_in_scope(), - info.target_in_scope() - ); - - match (info.source_in_scope(), info.target_in_scope()) { - (true, true) => { - self.sftp - .rename( - src.strip_prefix(&base).unwrap(), - dest.strip_prefix(&base).unwrap(), - None, - ) - .map_err(|_| CloudErrorKind::InvalidRequest)?; - } - (true, false) => {} - (false, true) => Err(CloudErrorKind::NotSupported)?, // TODO - (false, false) => Err(CloudErrorKind::InvalidRequest)?, - } + fn rename(&self, request: Request, ticket: ticket::Rename, info: info::Rename) -> CResult<()> { + let src = request.path(); + let dest = info.target_path(); + let base = get_client_path(); - ticket.pass().unwrap(); - Ok(()) - }(); + println!( + "rename {} to {}, source in scope: {}, target in scope: {}", + src.display(), + dest.display(), + info.source_in_scope(), + info.target_in_scope() + ); - if let Err(e) = res { - ticket.fail(e).unwrap(); + match (info.source_in_scope(), info.target_in_scope()) { + (true, true) => { + self.sftp + .rename( + src.strip_prefix(&base).unwrap(), + dest.strip_prefix(&base).unwrap(), + None, + ) + .map_err(|_| CloudErrorKind::InvalidRequest)?; + } + (true, false) => {} + (false, true) => Err(CloudErrorKind::NotSupported)?, // TODO + (false, false) => Err(CloudErrorKind::InvalidRequest)?, } + + ticket.pass().unwrap(); + Ok(()) } fn fetch_placeholders( @@ -266,7 +252,7 @@ impl SyncFilter for Filter { request: Request, ticket: ticket::FetchPlaceholders, info: info::FetchPlaceholders, - ) { + ) -> CResult<()> { println!( "fetch_placeholders {:?} {:?}", request.path(), @@ -276,7 +262,10 @@ impl SyncFilter for Filter { let client_path = get_client_path(); let parent = absolute.strip_prefix(&client_path).unwrap(); - let dirs = self.sftp.readdir(parent).unwrap(); + let dirs = self + .sftp + .readdir(parent) + .map_err(|_| CloudErrorKind::InvalidRequest)?; let mut placeholders = dirs .into_iter() .filter(|(path, _)| !Path::new(&client_path).join(path).exists()) @@ -305,6 +294,8 @@ impl SyncFilter for Filter { .collect::>(); ticket.pass_with_placeholder(&mut placeholders).unwrap(); + + Ok(()) } fn closed(&self, request: Request, info: info::Closed) { @@ -318,14 +309,11 @@ impl SyncFilter for Filter { fn validate_data( &self, _request: Request, - ticket: ticket::ValidateData, + _ticket: ticket::ValidateData, _info: info::ValidateData, - ) { + ) -> CResult<()> { println!("validate_data"); - #[allow(unused_must_use)] - { - ticket.fail(CloudErrorKind::NotSupported); - } + Err(CloudErrorKind::NotSupported) } fn cancel_fetch_placeholders(&self, _request: Request, _info: info::CancelFetchPlaceholders) { @@ -336,12 +324,14 @@ impl SyncFilter for Filter { println!("opened: {:?}", request.path()); } - fn dehydrate(&self, _request: Request, ticket: ticket::Dehydrate, _info: info::Dehydrate) { + fn dehydrate( + &self, + _request: Request, + _ticket: ticket::Dehydrate, + _info: info::Dehydrate, + ) -> CResult<()> { println!("dehydrate"); - #[allow(unused_must_use)] - { - ticket.fail(CloudErrorKind::NotSupported); - } + Err(CloudErrorKind::NotSupported) } fn dehydrated(&self, _request: Request, _info: info::Dehydrated) { diff --git a/src/command/commands.rs b/src/command/commands.rs index e7b3d23..945f09c 100644 --- a/src/command/commands.rs +++ b/src/command/commands.rs @@ -1,4 +1,4 @@ -use std::{ops::Range, ptr, slice}; +use std::{ops::Range, ptr}; use windows::{ core, @@ -19,7 +19,6 @@ use crate::{ metadata::Metadata, placeholder_file::PlaceholderFile, request::{RawConnectionKey, RawTransferKey}, - usn::Usn, }; /// Read data from a placeholder file. @@ -162,29 +161,10 @@ pub struct CreatePlaceholders<'a> { impl Command for CreatePlaceholders<'_> { const OPERATION: CF_OPERATION_TYPE = CloudFilters::CF_OPERATION_TYPE_TRANSFER_PLACEHOLDERS; - type Result = Vec>; + type Result = (); type Field = CF_OPERATION_PARAMETERS_0_7; - unsafe fn result(info: CF_OPERATION_PARAMETERS_0) -> Self::Result { - // iterate over the placeholders and return, in a new vector, whether or - // not they were created with their new USN - if info.TransferPlaceholders.PlaceholderCount == 0 { - return vec![]; - } - - slice::from_raw_parts( - info.TransferPlaceholders.PlaceholderArray, - info.TransferPlaceholders.PlaceholderCount as usize, - ) - .iter() - .map(|placeholder| { - placeholder - .Result - .ok() - .map(|_| placeholder.CreateUsn as Usn) - }) - .collect() - } + unsafe fn result(_info: CF_OPERATION_PARAMETERS_0) -> Self::Result {} fn build(&self) -> CF_OPERATION_PARAMETERS_0 { CF_OPERATION_PARAMETERS_0 { diff --git a/src/error.rs b/src/error.rs index bc333ce..19d8649 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,8 @@ use windows::Win32::Foundation::{self, NTSTATUS}; +/// [SyncFilter][crate::filter::SyncFilter] trait callback result type. +pub type CResult = std::result::Result; + /// Predefined error types provided by the operating system. #[derive(Debug, Clone, Copy)] pub enum CloudErrorKind { diff --git a/src/filter/proxy.rs b/src/filter/proxy.rs index 08b1255..1357b85 100644 --- a/src/filter/proxy.rs +++ b/src/filter/proxy.rs @@ -7,6 +7,7 @@ use windows::Win32::Storage::CloudFilters::{ }; use crate::{ + command::{self, Fallible}, filter::{info, ticket, SyncFilter}, request::Request, }; @@ -82,13 +83,19 @@ pub unsafe extern "system" fn fetch_data( ) { if let Some(filter) = filter_from_info::(info) { let request = Request::new(*info); - let ticket = ticket::FetchData::new(request.connection_key(), request.transfer_key()); + let connection_key = request.connection_key(); + let transfer_key = request.transfer_key(); + let ticket = ticket::FetchData::new(connection_key, transfer_key); - filter.fetch_data( + let Err(e) = filter.fetch_data( request, ticket, info::FetchData((*params).Anonymous.FetchData), - ); + ) else { + return; + }; + + command::Write::fail(connection_key, transfer_key, e).unwrap(); } } @@ -98,13 +105,19 @@ pub unsafe extern "system" fn validate_data( ) { if let Some(filter) = filter_from_info::(info) { let request = Request::new(*info); - let ticket = ticket::ValidateData::new(request.connection_key(), request.transfer_key()); + let connection_key = request.connection_key(); + let transfer_key = request.transfer_key(); + let ticket = ticket::ValidateData::new(connection_key, transfer_key); - filter.validate_data( + let Err(e) = filter.validate_data( request, ticket, info::ValidateData((*params).Anonymous.ValidateData), - ); + ) else { + return; + }; + + command::Validate::fail(connection_key, transfer_key, e).unwrap(); } } @@ -126,14 +139,19 @@ pub unsafe extern "system" fn fetch_placeholders( ) { if let Some(filter) = filter_from_info::(info) { let request = Request::new(*info); - let ticket = - ticket::FetchPlaceholders::new(request.connection_key(), request.transfer_key()); + let connection_key = request.connection_key(); + let transfer_key = request.transfer_key(); + let ticket = ticket::FetchPlaceholders::new(connection_key, transfer_key); - filter.fetch_placeholders( + let Err(e) = filter.fetch_placeholders( request, ticket, info::FetchPlaceholders((*params).Anonymous.FetchPlaceholders), - ); + ) else { + return; + }; + + command::CreatePlaceholders::fail(connection_key, transfer_key, e).unwrap(); } } @@ -179,13 +197,19 @@ pub unsafe extern "system" fn notify_dehydrate( ) { if let Some(filter) = filter_from_info::(info) { let request = Request::new(*info); - let ticket = ticket::Dehydrate::new(request.connection_key(), request.transfer_key()); + let connection_key = request.connection_key(); + let transfer_key = request.transfer_key(); + let ticket = ticket::Dehydrate::new(connection_key, transfer_key); - filter.dehydrate( + let Err(e) = filter.dehydrate( request, ticket, info::Dehydrate((*params).Anonymous.Dehydrate), - ); + ) else { + return; + }; + + command::Dehydrate::fail(connection_key, transfer_key, e).unwrap(); } } @@ -207,9 +231,16 @@ pub unsafe extern "system" fn notify_delete( ) { if let Some(filter) = filter_from_info::(info) { let request = Request::new(*info); - let ticket = ticket::Delete::new(request.connection_key(), request.transfer_key()); + let connection_key = request.connection_key(); + let transfer_key = request.transfer_key(); + let ticket = ticket::Delete::new(connection_key, transfer_key); - filter.delete(request, ticket, info::Delete((*params).Anonymous.Delete)); + let Err(e) = filter.delete(request, ticket, info::Delete((*params).Anonymous.Delete)) + else { + return; + }; + + command::Delete::fail(connection_key, transfer_key, e).unwrap(); } } @@ -231,10 +262,16 @@ pub unsafe extern "system" fn notify_rename( ) { if let Some(filter) = filter_from_info::(info) { let request = Request::new(*info); - let ticket = ticket::Rename::new(request.connection_key(), request.transfer_key()); + let connection_key = request.connection_key(); + let transfer_key = request.transfer_key(); + let ticket = ticket::Rename::new(connection_key, transfer_key); let info = info::Rename((*params).Anonymous.Rename, request.volume_letter()); - filter.rename(request, ticket, info); + let Err(e) = filter.rename(request, ticket, info) else { + return; + }; + + command::Rename::fail(connection_key, transfer_key, e).unwrap(); } } diff --git a/src/filter/sync_filter.rs b/src/filter/sync_filter.rs index 0adeee4..4b36830 100644 --- a/src/filter/sync_filter.rs +++ b/src/filter/sync_filter.rs @@ -1,5 +1,5 @@ use crate::{ - error::CloudErrorKind, + error::{CResult, CloudErrorKind}, filter::{info, ticket}, request::Request, }; @@ -11,47 +11,44 @@ use crate::{ pub trait SyncFilter: Send + Sync { /// A placeholder hydration has been requested. This means that the placeholder should be /// populated with its corresponding data on the remote. - fn fetch_data(&self, _request: Request, ticket: ticket::FetchData, _info: info::FetchData) { - #[allow(unused_must_use)] - { - ticket.fail(CloudErrorKind::NotSupported); - } + fn fetch_data( + &self, + _request: Request, + _ticket: ticket::FetchData, + _info: info::FetchData, + ) -> CResult<()> { + Err(CloudErrorKind::NotSupported) } /// A placeholder hydration request has been cancelled. fn cancel_fetch_data(&self, _request: Request, _info: info::CancelFetchData) {} - /// Followed by a successful call to [SyncFilter::fetch_data][crate::SyncFilter::fetch_data], this callback should verify the integrity of + /// Followed by a successful call to [SyncFilter::fetch_data][super::SyncFilter::fetch_data], this callback should verify the integrity of /// the data persisted in the placeholder. /// - /// **You** are responsible for validating the data in the placeholder. To approve or - /// disapprove the request, use the ticket provided. + /// **You** are responsible for validating the data in the placeholder. To approve + /// the request, use the ticket provided. /// - /// Note that this callback is only called if [HydrationPolicy::require_validation][crate::HydrationPolicy::require_validation] is specified. + /// Note that this callback is only called if [HydrationPolicy::require_validation][crate::root::HydrationPolicy::require_validation] + /// is specified. fn validate_data( &self, _request: Request, - ticket: ticket::ValidateData, + _ticket: ticket::ValidateData, _info: info::ValidateData, - ) { - #[allow(unused_must_use)] - { - ticket.fail(CloudErrorKind::NotSupported); - } + ) -> CResult<()> { + Err(CloudErrorKind::NotSupported) } /// A directory population has been requested. The behavior of this callback is dependent on - /// the [PopulationType][crate::PopulationType] variant specified during registration. + /// the [PopulationType][crate::root::PopulationType] variant specified during registration. fn fetch_placeholders( &self, _request: Request, - ticket: ticket::FetchPlaceholders, + _ticket: ticket::FetchPlaceholders, _info: info::FetchPlaceholders, - ) { - #[allow(unused_must_use)] - { - ticket.fail(CloudErrorKind::NotSupported); - } + ) -> CResult<()> { + Err(CloudErrorKind::NotSupported) } /// A directory population request has been cancelled. @@ -69,12 +66,14 @@ pub trait SyncFilter: Send + Sync { /// the file will be __completely__ discarded. /// /// The operating system will handle dehydrating placeholder files automatically. However, it - /// is up to **you** to approve this. Use the ticket to approve or disapprove the request. - fn dehydrate(&self, _request: Request, ticket: ticket::Dehydrate, _info: info::Dehydrate) { - #[allow(unused_must_use)] - { - ticket.fail(CloudErrorKind::NotSupported); - } + /// is up to **you** to approve this. Use the ticket to approve the request. + fn dehydrate( + &self, + _request: Request, + _ticket: ticket::Dehydrate, + _info: info::Dehydrate, + ) -> CResult<()> { + Err(CloudErrorKind::NotSupported) } /// A placeholder dehydration request has been cancelled. @@ -83,12 +82,14 @@ pub trait SyncFilter: Send + Sync { /// A placeholder file is about to be deleted. /// /// The operating system will handle deleting placeholder files automatically. However, it is - /// up to **you** to approve this. Use the ticket to approve or disapprove the request. - fn delete(&self, _request: Request, ticket: ticket::Delete, _info: info::Delete) { - #[allow(unused_must_use)] - { - ticket.fail(CloudErrorKind::NotSupported); - } + /// up to **you** to approve this. Use the ticket to approve the request. + fn delete( + &self, + _request: Request, + _ticket: ticket::Delete, + _info: info::Delete, + ) -> CResult<()> { + Err(CloudErrorKind::NotSupported) } /// A placeholder file has been deleted. @@ -97,15 +98,17 @@ pub trait SyncFilter: Send + Sync { /// A placeholder file is about to be renamed or moved. /// /// The operating system will handle moving and renaming placeholder files automatically. - /// However, it is up to **you** to approve this. Use the ticket to approve or disapprove the + /// However, it is up to **you** to approve this. Use the ticket to approve the /// request. /// /// When the operation is completed, the [SyncFilter::renamed][crate::SyncFilter::renamed] callback will be called. - fn rename(&self, _request: Request, ticket: ticket::Rename, _info: info::Rename) { - #[allow(unused_must_use)] - { - ticket.fail(CloudErrorKind::NotSupported); - } + fn rename( + &self, + _request: Request, + _ticket: ticket::Rename, + _info: info::Rename, + ) -> CResult<()> { + Err(CloudErrorKind::NotSupported) } /// A placeholder file has been renamed or moved. diff --git a/src/filter/ticket.rs b/src/filter/ticket.rs index 9375c67..972dc72 100644 --- a/src/filter/ticket.rs +++ b/src/filter/ticket.rs @@ -6,13 +6,10 @@ use windows::{ }; use crate::{ - command::{self, Command, Fallible}, - error::CloudErrorKind, + command::{self, Command}, placeholder_file::PlaceholderFile, request::{RawConnectionKey, RawTransferKey}, - sealed, - usn::Usn, - utility, + sealed, utility, }; /// A ticket for the [SyncFilter::fetch_data][crate::SyncFilter::fetch_data] callback. @@ -31,11 +28,6 @@ impl FetchData { } } - /// Fail the callback with the specified error. - pub fn fail(&self, error_kind: CloudErrorKind) -> core::Result<()> { - command::Write::fail(self.connection_key, self.transfer_key, error_kind) - } - /// Displays a progress bar next to the file in the file explorer to show the progress of the /// current operation. In addition, the standard Windows file progress dialog will open /// displaying the speed and progress based on the values set. During background hydrations, @@ -53,7 +45,7 @@ impl FetchData { Ok(()) } - // TODO: response Command::Update + // TODO: response command::Update } impl utility::ReadAt for FetchData { @@ -116,12 +108,7 @@ impl ValidateData { command::Validate { range }.execute(self.connection_key, self.transfer_key) } - /// Fail the callback with the specified error. - pub fn fail(&self, error_kind: CloudErrorKind) -> core::Result<()> { - command::Validate::fail(self.connection_key, self.transfer_key, error_kind) - } - - // TODO: response Command::Update + // TODO: response command::Update } impl utility::ReadAt for ValidateData { @@ -161,22 +148,13 @@ impl FetchPlaceholders { /// Creates a list of placeholder files/directorys on the file system. /// /// The value returned is the final [Usn][crate::Usn] (and if they succeeded) after each placeholder is created. - pub fn pass_with_placeholder( - &self, - placeholders: &mut [PlaceholderFile], - ) -> core::Result>> { + pub fn pass_with_placeholder(&self, placeholders: &mut [PlaceholderFile]) -> core::Result<()> { command::CreatePlaceholders { total: placeholders.len() as _, placeholders, } .execute(self.connection_key, self.transfer_key) } - - /// Fail the callback with the specified error. - pub fn fail(&self, error_kind: CloudErrorKind) -> core::Result<()> { - command::CreatePlaceholders::fail(self.connection_key, self.transfer_key, error_kind) - .and(Ok(())) - } } /// A ticket for the [SyncFilter::dehydrate][crate::SyncFilter::dehydrate] callback. @@ -204,11 +182,6 @@ impl Dehydrate { pub fn pass_with_blob(&self, blob: &[u8]) -> core::Result<()> { command::Dehydrate { blob }.execute(self.connection_key, self.transfer_key) } - - /// Fail the callback with the specified error. - pub fn fail(&self, error_kind: CloudErrorKind) -> core::Result<()> { - command::Dehydrate::fail(self.connection_key, self.transfer_key, error_kind) - } } /// A ticket for the [SyncFilter::delete][crate::SyncFilter::delete] callback. @@ -231,11 +204,6 @@ impl Delete { pub fn pass(&self) -> core::Result<()> { command::Delete.execute(self.connection_key, self.transfer_key) } - - /// Fail the callback with the specified error. - pub fn fail(&self, error_kind: CloudErrorKind) -> core::Result<()> { - command::Delete::fail(self.connection_key, self.transfer_key, error_kind) - } } /// A ticket for the [SyncFilter::rename][crate::SyncFilter::rename] callback. @@ -258,9 +226,4 @@ impl Rename { pub fn pass(&self) -> core::Result<()> { command::Rename.execute(self.connection_key, self.transfer_key) } - - /// Fail the callback with the specified error. - pub fn fail(&self, error_kind: CloudErrorKind) -> core::Result<()> { - command::Rename::fail(self.connection_key, self.transfer_key, error_kind) - } } From 2418bda3645d2a4fdffe2e13af0fbe9ca937cfde Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Sat, 29 Jun 2024 21:42:34 +0800 Subject: [PATCH 09/34] feat: placeholder auxiblity functions --- src/ext/file.rs | 169 ++----------------------------- src/ext/mod.rs | 2 +- src/ext/path.rs | 12 +++ src/placeholder.rs | 242 +++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 255 insertions(+), 170 deletions(-) diff --git a/src/ext/file.rs b/src/ext/file.rs index c35f52e..561ac1b 100644 --- a/src/ext/file.rs +++ b/src/ext/file.rs @@ -1,6 +1,6 @@ use std::{ fs::File, - mem::{self, MaybeUninit}, + mem, ops::{Bound, RangeBounds}, os::windows::{io::AsRawHandle, prelude::RawHandle}, ptr, @@ -11,48 +11,20 @@ use windows::{ core, Win32::{ Foundation::HANDLE, - Storage::{ - CloudFilters::{ - self, CfDehydratePlaceholder, CfGetPlaceholderRangeInfo, - CfGetPlaceholderStateFromFileInfo, CfGetSyncRootInfoByHandle, CfHydratePlaceholder, - CfSetInSyncState, CF_PLACEHOLDER_RANGE_INFO_CLASS, CF_PLACEHOLDER_STATE, - CF_SYNC_PROVIDER_STATUS, CF_SYNC_ROOT_STANDARD_INFO, - }, - FileSystem::{self, GetFileInformationByHandleEx, FILE_ATTRIBUTE_TAG_INFO}, + Storage::CloudFilters::{ + self, CfDehydratePlaceholder, CfGetSyncRootInfoByHandle, CF_SYNC_PROVIDER_STATUS, + CF_SYNC_ROOT_STANDARD_INFO, }, }, }; use crate::{ root::{HydrationPolicy, HydrationType, PopulationType, SupportedAttributes}, - usn::Usn, + sealed::Sealed, }; /// An API extension to [File][std::fs::File]. -pub trait FileExt: AsRawHandle { - /// Hydrates a placeholder file. - // TODO: doc restrictions. I believe the remarks are wrong in that this call requires both read - // and write access? https://docs.microsoft.com/en-us/windows/win32/api/cfapi/nf-cfapi-cfhydrateplaceholder#remarks - fn hydrate>(&self, range: T) -> core::Result<()> { - unsafe { - CfHydratePlaceholder( - HANDLE(self.as_raw_handle() as isize), - match range.start_bound() { - Bound::Included(x) => *x as i64, - Bound::Excluded(x) => x.saturating_add(1) as i64, - Bound::Unbounded => 0, - }, - match range.end_bound() { - Bound::Included(x) => *x as i64, - Bound::Excluded(x) => x.saturating_sub(1) as i64, - Bound::Unbounded => -1, - }, - CloudFilters::CF_HYDRATE_FLAG_NONE, - ptr::null_mut(), - ) - } - } - +pub trait FileExt: AsRawHandle + Sealed { /// Dehydrates a placeholder file. fn dehydrate>(&self, range: T) -> core::Result<()> { dehydrate(self.as_raw_handle(), range, false) @@ -64,66 +36,6 @@ pub trait FileExt: AsRawHandle { dehydrate(self.as_raw_handle(), range, true) } - /// Reads raw data in a placeholder file without invoking the [SyncFilter][crate::SyncFilter]. - fn read_raw(&self, read_type: ReadType, offset: u64, buffer: &mut [u8]) -> core::Result { - // TODO: buffer length must be u32 max - let mut length = 0; - unsafe { - CfGetPlaceholderRangeInfo( - HANDLE(self.as_raw_handle() as isize), - read_type.into(), - offset as i64, - buffer.len() as i64, - buffer as *mut _ as *mut _, - buffer.len() as u32, - &mut length as *mut _, - ) - } - .map(|_| length) - } - - /// Gets the current state of the placeholder. - // TODO: test to ensure this works. I feel like returning an option here is a little odd in the - // case of a non parsable state. - fn placeholder_state(&self) -> core::Result> { - let mut info = MaybeUninit::::zeroed(); - unsafe { - GetFileInformationByHandleEx( - HANDLE(self.as_raw_handle() as isize), - FileSystem::FileAttributeTagInfo, - info.as_mut_ptr() as *mut _, - mem::size_of::() as u32, - ) - .ok()?; - - PlaceholderState::try_from_win32(CfGetPlaceholderStateFromFileInfo( - &info.assume_init() as *const _ as *const _, - FileSystem::FileAttributeTagInfo, - )) - } - } - - /// Marks a placeholder as synced. - /// - /// If the passed [USN][crate::Usn] is outdated, the call will fail. - // TODO: must have write access - fn mark_sync(&self, usn: Usn) -> core::Result { - mark_sync_state(self.as_raw_handle(), true, usn) - } - - /// Marks a placeholder as not in sync. - /// - /// If the passed [USN][crate::Usn] is outdated, the call will fail. - // TODO: must have write access - fn mark_unsync(&self, usn: Usn) -> core::Result { - mark_sync_state(self.as_raw_handle(), false, usn) - } - - /// Returns whether or not the handle is a valid placeholder. - fn is_placeholder(&self) -> core::Result { - self.placeholder_state().map(|state| state.is_some()) - } - /// Gets various characteristics of the sync root. fn sync_root_info(&self) -> core::Result { // TODO: this except finds the size after 2 calls of CfGetSyncRootInfoByHandle @@ -162,25 +74,6 @@ pub trait FileExt: AsRawHandle { } } -fn mark_sync_state(handle: RawHandle, sync: bool, usn: Usn) -> core::Result { - // TODO: docs say the usn NEEDS to be a null pointer? Why? Is it not supported? - // https://docs.microsoft.com/en-us/windows/win32/api/cfapi/nf-cfapi-cfsetinsyncstate - let mut usn = usn as i64; - unsafe { - CfSetInSyncState( - HANDLE(handle as isize), - if sync { - CloudFilters::CF_IN_SYNC_STATE_IN_SYNC - } else { - CloudFilters::CF_IN_SYNC_STATE_NOT_IN_SYNC - }, - CloudFilters::CF_SET_IN_SYNC_FLAG_NONE, - &mut usn as *mut _, - ) - .map(|_| usn as u64) - } -} - // TODO: is `CfDehydratePlaceholder` deprecated? // https://docs.microsoft.com/en-us/answers/questions/723805/what-is-the-behavior-of-file-ranges-in-different-p.html fn dehydrate>( @@ -214,26 +107,7 @@ fn dehydrate>( impl FileExt for File {} -/// The type of data to read from a placeholder. -#[derive(Debug, Copy, Clone)] -pub enum ReadType { - /// Any data that is saved to the disk. - Saved, - /// Data that has been synced to the cloud. - Validated, - /// Data that has not synced to the cloud. - Modified, -} - -impl From for CF_PLACEHOLDER_RANGE_INFO_CLASS { - fn from(read_type: ReadType) -> Self { - match read_type { - ReadType::Saved => CloudFilters::CF_PLACEHOLDER_RANGE_INFO_ONDISK, - ReadType::Validated => CloudFilters::CF_PLACEHOLDER_RANGE_INFO_VALIDATED, - ReadType::Modified => CloudFilters::CF_PLACEHOLDER_RANGE_INFO_MODIFIED, - } - } -} +impl Sealed for File {} /// Information about a sync root. #[derive(Debug)] @@ -360,32 +234,3 @@ impl From for CF_SYNC_PROVIDER_STATUS { } } } - -// TODO: I don't think this is an enum -#[derive(Debug, Clone, Copy)] -pub enum PlaceholderState { - Placeholder, - SyncRoot, - EssentialPropPresent, - InSync, - StatePartial, - PartiallyOnDisk, -} - -impl PlaceholderState { - fn try_from_win32(value: CF_PLACEHOLDER_STATE) -> core::Result> { - match value { - CloudFilters::CF_PLACEHOLDER_STATE_NO_STATES => Ok(None), - CloudFilters::CF_PLACEHOLDER_STATE_PLACEHOLDER => Ok(Some(Self::Placeholder)), - CloudFilters::CF_PLACEHOLDER_STATE_SYNC_ROOT => Ok(Some(Self::SyncRoot)), - CloudFilters::CF_PLACEHOLDER_STATE_ESSENTIAL_PROP_PRESENT => { - Ok(Some(Self::EssentialPropPresent)) - } - CloudFilters::CF_PLACEHOLDER_STATE_IN_SYNC => Ok(Some(Self::InSync)), - CloudFilters::CF_PLACEHOLDER_STATE_PARTIAL => Ok(Some(Self::StatePartial)), - CloudFilters::CF_PLACEHOLDER_STATE_PARTIALLY_ON_DISK => Ok(Some(Self::PartiallyOnDisk)), - CloudFilters::CF_PLACEHOLDER_STATE_INVALID => Err(core::Error::from_win32()), - _ => unreachable!(), - } - } -} diff --git a/src/ext/mod.rs b/src/ext/mod.rs index 333c997..a602d3c 100644 --- a/src/ext/mod.rs +++ b/src/ext/mod.rs @@ -1,5 +1,5 @@ mod file; mod path; -pub use file::{FileExt, PlaceholderState, ProviderStatus, SyncRootInfo}; +pub use file::{FileExt, ProviderStatus, SyncRootInfo}; pub use path::PathExt; diff --git a/src/ext/path.rs b/src/ext/path.rs index 84d3326..2107162 100644 --- a/src/ext/path.rs +++ b/src/ext/path.rs @@ -31,6 +31,18 @@ where .get()?, ) } + + // FIXME: This function is not work at all, the CF_PLACEHOLDER_STATE always be 0 or 1 + // fn placeholder_state(&self) -> core::Result { + // let path = U16CString::from_os_str(self.as_ref()).unwrap(); + // let mut file_data = MaybeUninit::zeroed(); + // unsafe { + // FindFirstFileW(PCWSTR(path.as_ptr()), file_data.as_mut_ptr()); + // Ok(CfGetPlaceholderStateFromFindData( + // file_data.assume_init_ref() as *const _ as *const _, + // )) + // } + // } } impl> PathExt for T {} diff --git a/src/placeholder.rs b/src/placeholder.rs index 985c30e..f73d410 100644 --- a/src/placeholder.rs +++ b/src/placeholder.rs @@ -1,8 +1,9 @@ use std::{ + fmt::Debug, fs::File, - mem, - ops::Range, - os::windows::io::{FromRawHandle, IntoRawHandle}, + mem::{self, MaybeUninit}, + ops::{Bound, Range, RangeBounds}, + os::windows::io::{AsRawHandle, FromRawHandle, IntoRawHandle, RawHandle}, path::Path, ptr, }; @@ -11,12 +12,17 @@ use widestring::U16CString; use windows::{ core::{self, PCWSTR}, Win32::{ - Foundation::{CloseHandle, ERROR_NOT_A_CLOUD_FILE, HANDLE}, + Foundation::{ + CloseHandle, BOOL, ERROR_NOT_A_CLOUD_FILE, E_HANDLE, HANDLE, INVALID_HANDLE_VALUE, + }, Storage::CloudFilters::{ self, CfCloseHandle, CfConvertToPlaceholder, CfGetPlaceholderInfo, - CfOpenFileWithOplock, CfRevertPlaceholder, CfSetInSyncState, CfSetPinState, - CfUpdatePlaceholder, CF_CONVERT_FLAGS, CF_FILE_RANGE, CF_OPEN_FILE_FLAGS, CF_PIN_STATE, - CF_PLACEHOLDER_STANDARD_INFO, CF_SET_PIN_FLAGS, CF_UPDATE_FLAGS, + CfGetPlaceholderRangeInfo, CfGetWin32HandleFromProtectedHandle, CfHydratePlaceholder, + CfOpenFileWithOplock, CfReferenceProtectedHandle, CfReleaseProtectedHandle, + CfRevertPlaceholder, CfSetInSyncState, CfSetPinState, CfUpdatePlaceholder, + CF_CONVERT_FLAGS, CF_FILE_RANGE, CF_OPEN_FILE_FLAGS, CF_PIN_STATE, + CF_PLACEHOLDER_RANGE_INFO_CLASS, CF_PLACEHOLDER_STANDARD_INFO, CF_SET_PIN_FLAGS, + CF_UPDATE_FLAGS, }, }, }; @@ -86,6 +92,54 @@ impl Drop for OwnedPlaceholderHandle { } } +/// Holds a Win32 handle from the protected handle. +/// +/// The reference count will increase when the [ArcWin32Handle] is cloned +/// and decrease when the [ArcWin32Handle] is dropped. +pub struct ArcWin32Handle { + win32_handle: HANDLE, + protected_handle: HANDLE, +} + +impl ArcWin32Handle { + /// Win32 handle from the protected handle. + pub fn handle(&self) -> HANDLE { + self.win32_handle + } +} + +impl Clone for ArcWin32Handle { + fn clone(&self) -> Self { + if self.protected_handle != INVALID_HANDLE_VALUE { + unsafe { CfReferenceProtectedHandle(self.protected_handle) }; + } + + Self { + win32_handle: self.win32_handle, + protected_handle: self.protected_handle, + } + } +} + +impl AsRawHandle for ArcWin32Handle { + fn as_raw_handle(&self) -> RawHandle { + unsafe { mem::transmute(self.win32_handle) } + } +} + +impl Drop for ArcWin32Handle { + fn drop(&mut self) { + if self.protected_handle != INVALID_HANDLE_VALUE { + unsafe { CfReleaseProtectedHandle(self.protected_handle) }; + } + } +} + +/// Safety: reference counted by syscall +unsafe impl Send for ArcWin32Handle {} +/// Safety: reference counted by syscall +unsafe impl Sync for ArcWin32Handle {} + /// Options for opening a placeholder file/directory. pub struct OpenOptions { flags: CF_OPEN_FILE_FLAGS, @@ -306,9 +360,11 @@ impl PlaceholderInfo { pub fn validated_data_size(&self) -> i64 { unsafe { &*self.info }.ValidatedDataSize } + pub fn modified_data_size(&self) -> i64 { unsafe { &*self.info }.ModifiedDataSize } + pub fn properties_size(&self) -> i64 { unsafe { &*self.info }.PropertiesSize } @@ -469,6 +525,77 @@ impl Default for UpdateOptions<'_> { } } +/// The type of data to read from a placeholder. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ReadType { + /// Any data that is saved to the disk. + Any, + /// Data that has been synced to the cloud. + Validated, + /// Data that has not synced to the cloud. + Modified, +} + +impl From for CF_PLACEHOLDER_RANGE_INFO_CLASS { + fn from(read_type: ReadType) -> Self { + match read_type { + ReadType::Any => CloudFilters::CF_PLACEHOLDER_RANGE_INFO_ONDISK, + ReadType::Validated => CloudFilters::CF_PLACEHOLDER_RANGE_INFO_VALIDATED, + ReadType::Modified => CloudFilters::CF_PLACEHOLDER_RANGE_INFO_MODIFIED, + } + } +} + +// #[derive(Clone, Copy)] +// pub struct PlaceholderState(CF_PLACEHOLDER_STATE); + +// impl PlaceholderState { +// /// The placeholder is both a directory as well as the sync root. +// pub fn sync_root(&self) -> bool { +// (self.0 & CloudFilters::CF_PLACEHOLDER_STATE_SYNC_ROOT).0 != 0 +// } + +// /// There exists an essential property in the property store of the file or directory. +// pub fn essential_prop_present(&self) -> bool { +// (self.0 & CloudFilters::CF_PLACEHOLDER_STATE_ESSENTIAL_PROP_PRESENT).0 != 0 +// } + +// /// The placeholder is in sync. +// pub fn in_sync(&self) -> bool { +// (self.0 & CloudFilters::CF_PLACEHOLDER_STATE_IN_SYNC).0 != 0 +// } + +// /// The placeholder content is not ready to be consumed by the user application, +// /// though it may or may not be fully present locally. +// /// +// /// An example is a placeholder file whose content has been fully downloaded to the local disk, +// /// but is yet to be validated by a sync provider that +// /// has registered the sync root with the hydration modifier +// /// [HydrationPolicy::require_validation][crate::root::HydrationPolicy::require_validation]. +// pub fn partial(&self) -> bool { +// (self.0 & CloudFilters::CF_PLACEHOLDER_STATE_PARTIAL).0 != 0 +// } + +// /// The placeholder content is not fully present locally. +// /// +// /// When this is set, [PlaceholderState::partial] also be `true`. +// pub fn partial_on_disk(&self) -> bool { +// (self.0 & CloudFilters::CF_PLACEHOLDER_STATE_PARTIALLY_ON_DISK).0 != 0 +// } +// } + +// impl Debug for PlaceholderState { +// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// f.debug_struct("PlaceholderState") +// .field("sync_root", &self.sync_root()) +// .field("essential_prop_present", &self.essential_prop_present()) +// .field("in_sync", &self.in_sync()) +// .field("partial", &self.partial()) +// .field("partial_on_disk", &self.partial_on_disk()) +// .finish() +// } +// } + /// A struct to perform various operations on a placeholder(or regular) file/directory. #[derive(Debug)] pub struct Placeholder { @@ -617,6 +744,107 @@ impl Placeholder { Ok(self) } + + /// Retrieves data from a placeholder. + pub fn retrieve_data( + &self, + read_type: ReadType, + offset: u64, + buffer: &mut [u8], + ) -> core::Result { + let mut length = MaybeUninit::zeroed(); + unsafe { + CfGetPlaceholderRangeInfo( + self.handle.handle, + read_type.into(), + offset as i64, + buffer.len() as i64, + buffer as *mut _ as *mut _, + buffer.len() as u32, + length.assume_init_mut(), + ) + .map(|_| length.assume_init()) + } + } + + // FIXME: This function is not work at all, the CF_PLACEHOLDER_STATE always be 0 or 1 + // pub fn state(&self) -> core::Result> { + // let mut info = MaybeUninit::::zeroed(); + // let win32_handle = self.win32_handle()?; + // let state = unsafe { + // GetFileInformationByHandleEx( + // win32_handle.win32_handle, + // FileSystem::FileAttributeTagInfo, + // info.as_mut_ptr() as *mut _, + // mem::size_of::() as u32, + // ) + // .ok() + // .inspect_err(|e| println!("GetFileInformationByHandleEx: {e:#?}"))?; + + // CfGetPlaceholderStateFromFileInfo( + // info.assume_init_ref() as *const _ as *const _, + // FileSystem::FileAttributeTagInfo, + // ) + // }; + + // match state { + // CloudFilters::CF_PLACEHOLDER_STATE_INVALID => Err(core::Error::from_win32()), + // CloudFilters::CF_PLACEHOLDER_STATE_NO_STATES => Ok(None), + // s => Ok(Some(PlaceholderState(s))), + // } + // } + + /// Returns the Win32 handle from protected handle. + /// + /// Returns `Err(E_HANDLE)` if the [OwnedPlaceholderHandle::handle_type] is not [PlaceholderHandleType::CfApi]. + pub fn win32_handle(&self) -> core::Result { + let (handle, win32_handle) = match self.handle.handle_type { + PlaceholderHandleType::CfApi => { + let win32_handle = unsafe { + CfReferenceProtectedHandle(self.handle.handle).ok()?; + CfGetWin32HandleFromProtectedHandle(self.handle.handle) + }; + BOOL::from(!win32_handle.is_invalid()).ok()?; + (self.handle.handle, win32_handle) + } + PlaceholderHandleType::Win32 => Err(core::Error::from(E_HANDLE))?, + }; + + Ok(ArcWin32Handle { + win32_handle, + protected_handle: handle, + }) + } + + /// Hydrates a placeholder file by ensuring that the specified byte range is present on-disk + /// in the placeholder. This is valid for files only. + /// + /// # Panics + /// + /// Panics if the start bound is greater than [i64::MAX] or + /// the end bound sub start bound is greater than [i64::MAX]. + /// + /// See also [CfHydratePlaceholder](https://learn.microsoft.com/en-us/windows/win32/api/cfapi/nf-cfapi-cfhydrateplaceholder) + /// and [discussion](https://docs.microsoft.com/en-us/windows/win32/api/cfapi/nf-cfapi-cfhydrateplaceholder#remarks). + pub fn hydrate(&mut self, range: impl RangeBounds) -> core::Result<()> { + unsafe { + CfHydratePlaceholder( + self.handle.handle, + match range.start_bound() { + Bound::Included(x) => (*x).try_into().unwrap(), + Bound::Excluded(x) => (x + 1).try_into().unwrap(), + Bound::Unbounded => 0, + }, + match range.end_bound() { + Bound::Included(x) => (*x).try_into().unwrap(), + Bound::Excluded(x) => (x - 1).try_into().unwrap(), + Bound::Unbounded => -1, + }, + CloudFilters::CF_HYDRATE_FLAG_NONE, + ptr::null_mut(), + ) + } + } } impl From for Placeholder { From b545b72fc722cdc0b0e5ba55f20e19cc096242cf Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Sun, 30 Jun 2024 02:02:56 +0800 Subject: [PATCH 10/34] feat: SyncFilter::state_changed --- examples/sftp/src/main.rs | 6 +- src/filter/sync_filter.rs | 12 ++++ src/placeholder.rs | 15 ++++- src/root/connect.rs | 41 +++++++----- src/root/session.rs | 133 ++++++++++++++++++++++++++++++++++++-- 5 files changed, 182 insertions(+), 25 deletions(-) diff --git a/examples/sftp/src/main.rs b/examples/sftp/src/main.rs index 7ec3f55..f55a353 100644 --- a/examples/sftp/src/main.rs +++ b/examples/sftp/src/main.rs @@ -80,7 +80,7 @@ fn main() { wait_for_ctrlc(); - connection.disconnect().unwrap(); + drop(connection); sync_root_id.unregister().unwrap(); } @@ -341,6 +341,10 @@ impl SyncFilter for Filter { fn renamed(&self, _request: Request, _info: info::Renamed) { println!("renamed"); } + + fn state_changed(&self, changes: Vec) { + println!("state_changed: {:?}", changes); + } } #[derive(Error, Debug)] diff --git a/src/filter/sync_filter.rs b/src/filter/sync_filter.rs index 4b36830..46a334a 100644 --- a/src/filter/sync_filter.rs +++ b/src/filter/sync_filter.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use crate::{ error::{CResult, CloudErrorKind}, filter::{info, ticket}, @@ -113,4 +115,14 @@ pub trait SyncFilter: Send + Sync { /// A placeholder file has been renamed or moved. fn renamed(&self, _request: Request, _info: info::Renamed) {} + + /// Placeholder for changed attributes under the sync root. + /// + /// This callback is implemented using [ReadDirectoryChangesW][https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw] + /// so it is not provided by the `Cloud Filter APIs`. + /// + /// This callback is used to detect when a user pins or unpins a placeholder file, etc. + /// + /// See also [Cloud Files API Frequently Asked Questions](https://www.userfilesystem.com/programming/faq/). + fn state_changed(&self, _changes: Vec) {} } diff --git a/src/placeholder.rs b/src/placeholder.rs index f73d410..abf19b1 100644 --- a/src/placeholder.rs +++ b/src/placeholder.rs @@ -292,7 +292,6 @@ impl ConvertOptions { self } - // TODO: make the name of this function more specific /// Marks the placeholder as "partially full," such that [SyncFilter::fetch_placeholders][crate::SyncFilter::fetch_placeholders] /// will be invoked when this directory is next accessed so that the remaining placeholders are inserted. /// @@ -603,6 +602,15 @@ pub struct Placeholder { } impl Placeholder { + /// Create a placeholder from a raw handle. + /// + /// # Safety + /// + /// The passed handle must be a valid protected handle or win32 handle. + pub unsafe fn from_raw_handle(handle: OwnedPlaceholderHandle) -> Self { + Self { handle } + } + /// Open options for opening [Placeholder][crate::Placeholder]s. pub fn options() -> OpenOptions { OpenOptions::default() @@ -816,6 +824,11 @@ impl Placeholder { }) } + /// Returns the owned placeholder handle. + pub fn inner_handle(&self) -> &OwnedPlaceholderHandle { + &self.handle + } + /// Hydrates a placeholder file by ensuring that the specified byte range is present on-disk /// in the placeholder. This is valid for files only. /// diff --git a/src/root/connect.rs b/src/root/connect.rs index befc4bd..0e32da8 100644 --- a/src/root/connect.rs +++ b/src/root/connect.rs @@ -1,8 +1,11 @@ -use windows::{ - core, - Win32::Storage::CloudFilters::{CfDisconnectSyncRoot, CF_CONNECTION_KEY}, +use std::{ + sync::mpsc::Sender, + thread::{self, JoinHandle}, + time::Duration, }; +use windows::Win32::Storage::CloudFilters::{CfDisconnectSyncRoot, CF_CONNECTION_KEY}; + use crate::{filter::Callbacks, request::RawConnectionKey}; /// A handle to the current session for a given sync root. @@ -18,6 +21,10 @@ use crate::{filter::Callbacks, request::RawConnectionKey}; #[derive(Debug)] pub struct Connection { connection_key: RawConnectionKey, + + cancel_token: Sender<()>, + join_handle: JoinHandle<()>, + _callbacks: Callbacks, filter: T, } @@ -25,9 +32,17 @@ pub struct Connection { // this struct could house many more windows api functions, although they all seem to do nothing // according to the threads on microsoft q&a impl Connection { - pub(crate) fn new(connection_key: RawConnectionKey, callbacks: Callbacks, filter: T) -> Self { + pub(crate) fn new( + connection_key: RawConnectionKey, + cancel_token: Sender<()>, + join_handle: JoinHandle<()>, + callbacks: Callbacks, + filter: T, + ) -> Self { Self { connection_key, + cancel_token, + join_handle, _callbacks: callbacks, filter, } @@ -42,23 +57,15 @@ impl Connection { pub fn filter(&self) -> &T { &self.filter } - - /// Disconnects the sync root, read [Connection][crate::Connection] for more information. - pub fn disconnect(self) -> core::Result<()> { - self.disconnect_ref() - } - - #[inline] - fn disconnect_ref(&self) -> core::Result<()> { - unsafe { CfDisconnectSyncRoot(CF_CONNECTION_KEY(self.connection_key)) } - } } impl Drop for Connection { fn drop(&mut self) { - #[allow(unused_must_use)] - { - self.disconnect_ref(); + unsafe { CfDisconnectSyncRoot(CF_CONNECTION_KEY(self.connection_key)) }.unwrap(); + + _ = self.cancel_token.send(()); + while !self.join_handle.is_finished() { + thread::sleep(Duration::from_millis(150)); } } } diff --git a/src/root/session.rs b/src/root/session.rs index 96cd381..20e12c8 100644 --- a/src/root/session.rs +++ b/src/root/session.rs @@ -1,16 +1,34 @@ use std::{ ffi::OsString, - path::Path, - sync::{Arc, Weak}, + fs::OpenOptions, + mem::{self, MaybeUninit}, + os::windows::{fs::OpenOptionsExt, io::AsRawHandle}, + path::{Path, PathBuf}, + ptr, + sync::{ + mpsc::{self, Sender, TryRecvError}, + Arc, Weak, + }, + thread::{self, JoinHandle}, + time::Duration, }; +use widestring::U16Str; use windows::{ core, Win32::{ - Storage::CloudFilters::{self, CfConnectSyncRoot, CF_CONNECT_FLAGS}, + Foundation::{ERROR_IO_INCOMPLETE, HANDLE}, + Storage::{ + CloudFilters::{self, CfConnectSyncRoot, CF_CONNECT_FLAGS}, + FileSystem::{ + ReadDirectoryChangesW, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OVERLAPPED, + FILE_LIST_DIRECTORY, FILE_NOTIFY_CHANGE_ATTRIBUTES, FILE_NOTIFY_INFORMATION, + }, + }, System::{ Com::{self, CoCreateInstance}, Search::{self, ISearchCatalogManager, ISearchManager}, + IO::{CancelIoEx, GetOverlappedResult}, }, }, }; @@ -53,7 +71,7 @@ impl Session { let filter = Arc::new(filter); let callbacks = filter::callbacks::(); - unsafe { + let key = unsafe { CfConnectSyncRoot( path.as_ref().as_os_str(), callbacks.as_ptr(), @@ -66,8 +84,18 @@ impl Session { | CloudFilters::CF_CONNECT_FLAG_REQUIRE_FULL_FILE_PATH | CloudFilters::CF_CONNECT_FLAG_REQUIRE_PROCESS_INFO, ) - } - .map(|key| Connection::new(key.0, callbacks, filter)) + }?; + + let (cancel_token, join_handle) = + spawn_root_watcher(path.as_ref().to_path_buf(), filter.clone()); + + Ok(Connection::new( + key.0, + cancel_token, + join_handle, + callbacks, + filter, + )) } } @@ -95,3 +123,96 @@ fn index_path(path: &Path) -> core::Result<()> { crawler.SaveAll() } } + +fn spawn_root_watcher( + path: PathBuf, + filter: Arc, +) -> (Sender<()>, JoinHandle<()>) { + let (tx, rx) = mpsc::channel(); + let handle = thread::spawn(move || { + const CHANGE_BUF_SIZE: usize = 1024; + + let sync_root = OpenOptions::new() + .access_mode(FILE_LIST_DIRECTORY.0) + .custom_flags((FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED).0) + .open(&path) + .expect("sync root directory is opened"); + let mut changes_buf = MaybeUninit::<[u8; CHANGE_BUF_SIZE]>::zeroed(); + let mut overlapped = MaybeUninit::zeroed(); + let mut transferred = MaybeUninit::zeroed(); + + while matches!(rx.try_recv(), Err(TryRecvError::Empty)) { + unsafe { + ReadDirectoryChangesW( + HANDLE(sync_root.as_raw_handle() as _), + changes_buf.as_mut_ptr() as *mut _, + CHANGE_BUF_SIZE as _, + true, + FILE_NOTIFY_CHANGE_ATTRIBUTES, + ptr::null_mut(), + overlapped.as_mut_ptr(), + None, + ) + } + .ok() + .expect("read directory changes"); + + loop { + if unsafe { + !GetOverlappedResult( + HANDLE(sync_root.as_raw_handle() as _), + overlapped.as_mut_ptr(), + transferred.as_mut_ptr(), + false, + ) + } + .into() + { + let win32_err = core::Error::from_win32().win32_error(); + if win32_err != Some(ERROR_IO_INCOMPLETE) { + panic!( + "get overlapped result: {win32_err:?}, expected: {ERROR_IO_INCOMPLETE:?}" + ); + } + + // cancel by user + if !matches!(rx.try_recv(), Err(TryRecvError::Empty)) { + unsafe { + CancelIoEx( + HANDLE(sync_root.as_raw_handle() as _), + overlapped.as_mut_ptr(), + ) + }; + return; + } + + thread::sleep(Duration::from_millis(300)); + continue; + } + + if unsafe { transferred.assume_init() } == 0 { + break; + } + + let mut changes = Vec::with_capacity(8); + let mut entry = changes_buf.as_ptr() as *const FILE_NOTIFY_INFORMATION; + while !entry.is_null() { + let relative = unsafe { + U16Str::from_ptr( + &(*entry).FileName as *const _, + (*entry).FileNameLength as usize / mem::size_of::(), + ) + }; + + changes.push(path.join(relative.to_os_string())); + entry = (unsafe { *entry }).NextEntryOffset as *const _; + } + + filter.state_changed(changes); + break; + } + } + }); + + (tx, handle) +} From 27ca2f5d27358693af96cfc036634883786e47c7 Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Mon, 1 Jul 2024 11:12:28 +0800 Subject: [PATCH 11/34] chore: sync root --- src/root/connect.rs | 11 +--- src/root/mod.rs | 4 +- src/root/register.rs | 14 +---- src/root/{sync_root.rs => sync_root_id.rs} | 65 ++++++++++++++-------- 4 files changed, 51 insertions(+), 43 deletions(-) rename src/root/{sync_root.rs => sync_root_id.rs} (74%) diff --git a/src/root/connect.rs b/src/root/connect.rs index 0e32da8..23dd0dc 100644 --- a/src/root/connect.rs +++ b/src/root/connect.rs @@ -10,14 +10,9 @@ use crate::{filter::Callbacks, request::RawConnectionKey}; /// A handle to the current session for a given sync root. /// -/// By calling [Connection::disconnect][crate::Connection::disconnect], the session will terminate -/// and no more file operations will be able to be performed within the sync root. Note that this +/// [Connection] will disconnect when dropped. Note that this /// does **NOT** mean the sync root will be unregistered. To do so, call -/// [SyncRootId::unregister][crate::SyncRootId::unregister]. -/// -/// [Connection::disconnect][crate::Connection::disconnect] is called implicitly when the struct is -/// dropped. To handle possible errors, be sure to call -/// [Connection::disconnect][crate::Connection::disconnect] explicitly. +/// [SyncRootId::unregister][crate::root::SyncRootId::unregister]. #[derive(Debug)] pub struct Connection { connection_key: RawConnectionKey, @@ -53,7 +48,7 @@ impl Connection { self.connection_key } - /// A reference to the inner [SyncFilter][crate::SyncFilter] struct. + /// A reference to the inner [SyncFilter][crate::filter::SyncFilter] struct. pub fn filter(&self) -> &T { &self.filter } diff --git a/src/root/mod.rs b/src/root/mod.rs index 4596521..4730527 100644 --- a/src/root/mod.rs +++ b/src/root/mod.rs @@ -1,7 +1,7 @@ mod connect; mod register; mod session; -mod sync_root; +mod sync_root_id; pub use connect::Connection; pub use register::{ @@ -9,4 +9,4 @@ pub use register::{ SupportedAttributes, }; pub use session::Session; -pub use sync_root::{active_roots, is_supported, SecurityId, SyncRootId, SyncRootIdBuilder}; +pub use sync_root_id::{active_roots, is_supported, SecurityId, SyncRootId, SyncRootIdBuilder}; diff --git a/src/root/register.rs b/src/root/register.rs index e91cf5a..4232d6c 100644 --- a/src/root/register.rs +++ b/src/root/register.rs @@ -48,7 +48,7 @@ impl<'a> Registration<'a> { pub fn from_sync_root_id(sync_root_id: &'a SyncRootId) -> Self { Self { sync_root_id, - display_name: sync_root_id.as_u16str(), + display_name: &sync_root_id.as_u16_str(), recycle_bin_uri: None, show_siblings_as_group: false, allow_pinning: false, @@ -194,7 +194,7 @@ impl<'a> Registration<'a> { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ProtectionMode { Personal, Unknown, @@ -209,7 +209,7 @@ impl From for StorageProviderProtectionMode { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HydrationType { Partial, Progressive, @@ -361,14 +361,6 @@ impl SupportedAttributes { self.0 |= StorageProviderInSyncPolicy::DirectoryLastWriteTime; self } - - // TODO: I'm not sure how this differs from the default policy, - // https://docs.microsoft.com/en-us/answers/questions/760677/how-does-cf-insync-policy-none-differ-from-cf-insy.html - - pub fn none(mut self) -> Self { - self.0 |= StorageProviderInSyncPolicy::PreserveInsyncForSyncEngine; - self - } } impl Default for SupportedAttributes { diff --git a/src/root/sync_root.rs b/src/root/sync_root_id.rs similarity index 74% rename from src/root/sync_root.rs rename to src/root/sync_root_id.rs index 6feaeb3..4e2103d 100644 --- a/src/root/sync_root.rs +++ b/src/root/sync_root_id.rs @@ -1,9 +1,9 @@ -use std::{mem::MaybeUninit, path::Path, ptr}; +use std::{ffi::OsString, mem::MaybeUninit, os::windows::ffi::OsStringExt, path::Path, ptr}; use widestring::{U16CString, U16Str, U16String}; use windows::{ core::{self, HSTRING, PWSTR}, - Storage::Provider::StorageProviderSyncRootManager, + Storage::Provider::{StorageProviderSyncRootInfo, StorageProviderSyncRootManager}, Win32::{ Foundation::{self, GetLastError, HANDLE}, Security::{self, Authorization::ConvertSidToStringSidW, GetTokenInformation, TOKEN_USER}, @@ -15,15 +15,13 @@ use windows::{ use crate::ext::PathExt; /// Returns a list of active sync roots. -pub fn active_roots() { - // GetCurrentSyncRoots() - todo!() +pub fn active_roots() -> core::Result> { + StorageProviderSyncRootManager::GetCurrentSyncRoots().map(|list| list.into_iter().collect()) } /// Returns whether or not the Cloud Filter API is supported (or at least the UWP part of it, for /// now). pub fn is_supported() -> core::Result { - // TODO: Check current windows version to see if this function is supported before calling it StorageProviderSyncRootManager::IsSupported() } @@ -38,15 +36,27 @@ pub struct SyncRootIdBuilder { impl SyncRootIdBuilder { /// Create a new builder with the given provider name. /// - /// The provider name MUST NOT contain exclamation points and it must be within [255](https://docs.microsoft.com/en-us/windows/win32/api/cfapi/ns-cfapi-cf_sync_root_provider_info#remarks) characters. + /// The provider name MUST NOT contain exclamation points and it must be within + /// [255](https://docs.microsoft.com/en-us/windows/win32/api/cfapi/ns-cfapi-cf_sync_root_provider_info#remarks) characters. + /// + /// # Panics + /// + /// Panics if the provider name is longer than 255 characters or contains exclamation points. pub fn new(provider_name: U16String) -> Self { - // TODO: assert that is doesn't have exclamation points assert!( provider_name.len() <= CloudFilters::CF_MAX_PROVIDER_NAME_LENGTH as usize, "provider name must not exceed {} characters, got {} characters", CloudFilters::CF_MAX_PROVIDER_NAME_LENGTH, provider_name.len() ); + assert!( + provider_name + .as_slice() + .iter() + .find(|c| **c == SyncRootId::SEPARATOR) + .is_none(), + "provider name must not contain exclamation points" + ); Self { provider_name, @@ -56,7 +66,7 @@ impl SyncRootIdBuilder { } /// The security id of the Windows user. Retrieve this value via the - /// [SecurityId][crate::SecurityId] struct. + /// [SecurityId] struct. /// /// By default, a sync root registered without a user security id will be installed globally. pub fn user_security_id(mut self, security_id: SecurityId) -> Self { @@ -93,7 +103,7 @@ impl SyncRootIdBuilder { /// as specified /// [here](https://docs.microsoft.com/en-us/uwp/api/windows.storage.provider.storageprovidersyncrootinfo.id?view=winrt-22000#property-value). /// -/// A [SyncRootId][crate::SyncRootId] stores an inner, reference counted [HSTRING][windows::core::HSTRING], making this struct cheap to clone. +/// A [SyncRootId] stores an inner, reference counted [HSTRING][windows::core::HSTRING], making this struct cheap to clone. #[derive(Debug, Clone)] pub struct SyncRootId(HSTRING); @@ -102,13 +112,13 @@ impl SyncRootId { // unicode exclamation point as told in the specification above const SEPARATOR: u16 = 0x21; - /// Creates a [SyncRootId][crate::SyncRootId] from the sync root at the given path. + /// Creates a [SyncRootId] from the sync root at the given path. pub fn from_path>(path: P) -> core::Result { // if the id is coming from a sync root, then it must be valid Ok(Self(path.as_ref().sync_root_info()?.Id()?)) } - /// Whether or not the [SyncRootId][crate::SyncRootId] has already been registered. + /// Whether or not the [SyncRootId] has already been registered. pub fn is_registered(&self) -> core::Result { Ok( match StorageProviderSyncRootManager::GetSyncRootInformationForId(&self.0) { @@ -118,22 +128,32 @@ impl SyncRootId { ) } - /// Unregisters the sync root at the current [SyncRootId][crate::SyncRootId] if it exists. + /// Returns the sync root information for the [SyncRootId]. + pub fn sync_root_info(&self) -> core::Result { + StorageProviderSyncRootManager::GetSyncRootInformationForId(&self.0) + } + + /// Unregisters the sync root at the current [SyncRootId] if it exists. pub fn unregister(&self) -> core::Result<()> { StorageProviderSyncRootManager::Unregister(&self.0) } - /// A reference to the [SyncRootId][crate::SyncRootId] as a 16 bit string. - pub fn as_u16str(&self) -> &U16Str { + /// A reference to the [SyncRootId] as a 16 bit string. + pub fn to_os_string(&self) -> OsString { + OsString::from_wide(self.0.as_wide()) + } + + /// A reference to the [SyncRootId] as a 16 bit string. + pub fn as_u16_str(&self) -> &U16Str { U16Str::from_slice(self.0.as_wide()) } - /// A reference to the [SyncRootId][crate::SyncRootId] as an [HSTRING][windows::core::HSTRING] (its inner value). + /// A reference to the [SyncRootId] as an [HSTRING][windows::core::HSTRING] (its inner value). pub fn as_hstring(&self) -> &HSTRING { &self.0 } - /// The three components of a [SyncRootId][crate::SyncRootId] as described by the specification. + /// The three components of a [SyncRootId] as described by the specification. /// /// The order goes as follows: /// `(provider-id, security-id, account-name)` @@ -167,17 +187,18 @@ impl SecurityId { // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getcurrentthreadeffectivetoken const CURRENT_THREAD_EFFECTIVE_TOKEN: HANDLE = HANDLE(-6); - /// Creates a new [SecurityId][crate::SecurityId] without any assertions. - pub fn new_unchecked(id: U16String) -> Self { - Self(id) + /// Creates a new [SecurityId] without any assertions. + pub fn new(id: OsString) -> Self { + Self(id.into()) } - /// The [SecurityId][crate::SecurityId] for the logged in user. + /// The [SecurityId] for the logged in user. pub fn current_user() -> core::Result { unsafe { let mut token_size = 0; let mut token = MaybeUninit::::uninit(); + // get the token size if !GetTokenInformation( Self::CURRENT_THREAD_EFFECTIVE_TOKEN, Security::TokenUser, @@ -205,7 +226,7 @@ impl SecurityId { let string_sid = U16CString::from_ptr_str(sid.0).into_ustring(); LocalFree(sid.0 as isize); - Ok(SecurityId::new_unchecked(string_sid)) + Ok(SecurityId::new(string_sid.to_os_string())) } } } From 9248f2f7179ead0db0c404fcbca7cef189c37ee9 Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Mon, 1 Jul 2024 11:19:14 +0800 Subject: [PATCH 12/34] fix: clippy --- .github/workflows/check.yml | 2 +- src/root/register.rs | 2 +- src/root/sync_root_id.rs | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 015e24a..70a680f 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -25,7 +25,7 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: clippy - run: cargo clippy --all-features + run: cargo clippy --all-features --workspace env: RUSTFLAGS: "-Dwarnings" diff --git a/src/root/register.rs b/src/root/register.rs index 4232d6c..8a47ad4 100644 --- a/src/root/register.rs +++ b/src/root/register.rs @@ -48,7 +48,7 @@ impl<'a> Registration<'a> { pub fn from_sync_root_id(sync_root_id: &'a SyncRootId) -> Self { Self { sync_root_id, - display_name: &sync_root_id.as_u16_str(), + display_name: sync_root_id.as_u16_str(), recycle_bin_uri: None, show_siblings_as_group: false, allow_pinning: false, diff --git a/src/root/sync_root_id.rs b/src/root/sync_root_id.rs index 4e2103d..2684d29 100644 --- a/src/root/sync_root_id.rs +++ b/src/root/sync_root_id.rs @@ -50,11 +50,10 @@ impl SyncRootIdBuilder { provider_name.len() ); assert!( - provider_name + !provider_name .as_slice() .iter() - .find(|c| **c == SyncRootId::SEPARATOR) - .is_none(), + .any(|c| *c == SyncRootId::SEPARATOR), "provider name must not contain exclamation points" ); From 9e52c38359708a424d98b2da549749761ad982cb Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Mon, 1 Jul 2024 13:47:25 +0800 Subject: [PATCH 13/34] docs --- src/filter/info.rs | 20 +++++++++----------- src/filter/sync_filter.rs | 4 ++-- src/filter/ticket.rs | 29 +++++++++++++---------------- src/metadata.rs | 4 +++- src/root/session.rs | 7 +++---- src/root/sync_root_id.rs | 33 +++++++++++++++++++++++++-------- 6 files changed, 55 insertions(+), 42 deletions(-) diff --git a/src/filter/info.rs b/src/filter/info.rs index 2f77cba..4c3f87c 100644 --- a/src/filter/info.rs +++ b/src/filter/info.rs @@ -10,7 +10,7 @@ use windows::Win32::Storage::CloudFilters::{ CF_CALLBACK_PARAMETERS_0_9, }; -/// Information for the [SyncFilter::fetch_data][crate::SyncFilter::fetch_data] callback. +/// Information for the [SyncFilter::fetch_data][crate::filter::SyncFilter::fetch_data] callback. pub struct FetchData(pub(crate) CF_CALLBACK_PARAMETERS_0_6); impl FetchData { @@ -20,7 +20,7 @@ impl FetchData { } /// Whether or not the callback was called from an explicit hydration via - /// [FileExt::hydrate][crate::ext::FileExt::hydrate]. + /// [Placeholder::hydrate][crate::placeholder::Placeholder::hydrate]. pub fn explicit_hydration(&self) -> bool { (self.0.Flags & CloudFilters::CF_CALLBACK_FETCH_DATA_FLAG_EXPLICIT_HYDRATION).0 != 0 } @@ -64,13 +64,11 @@ impl Debug for FetchData { } } -/// Information for the [SyncFilter::cancel_fetch_data][crate::SyncFilter::cancel_fetch_data] callback. +/// Information for the [SyncFilter::cancel_fetch_data][crate::filter::SyncFilter::cancel_fetch_data] callback. pub struct CancelFetchData(pub(crate) CF_CALLBACK_PARAMETERS_0_0); impl CancelFetchData { /// Whether or not the callback failed as a result of the 60 second timeout. - /// - /// Read more [here][crate::Request::reset_timeout]. pub fn timeout(&self) -> bool { (self.0.Flags & CloudFilters::CF_CALLBACK_CANCEL_FLAG_IO_TIMEOUT).0 != 0 } @@ -99,7 +97,7 @@ impl Debug for CancelFetchData { } } -/// Information for the [SyncFilter::validate_data][crate::SyncFilter::validate_data] callback. +/// Information for the [SyncFilter::validate_data][crate::filter::SyncFilter::validate_data] callback. pub struct ValidateData(pub(crate) CF_CALLBACK_PARAMETERS_0_11); impl ValidateData { @@ -124,7 +122,7 @@ impl Debug for ValidateData { } } -/// Information for the [SyncFilter::fetch_placeholders][crate::SyncFilter::fetch_placeholders] +/// Information for the [SyncFilter::fetch_placeholders][crate::filter::SyncFilter::fetch_placeholders] /// callback. pub struct FetchPlaceholders(pub(crate) CF_CALLBACK_PARAMETERS_0_7); @@ -306,12 +304,12 @@ impl Debug for Delete { } } -/// Information for the [SyncFilter::deleted][crate::SyncFilter::deleted] callback. +/// Information for the [SyncFilter::deleted][crate::filter::SyncFilter::deleted] callback. #[derive(Debug)] #[allow(dead_code)] pub struct Deleted(pub(crate) CF_CALLBACK_PARAMETERS_0_4); -/// Information for the [SyncFilter::rename][crate::SyncFilter::rename] callback. +/// Information for the [SyncFilter::rename][crate::filter::SyncFilter::rename] callback. pub struct Rename(pub(crate) CF_CALLBACK_PARAMETERS_0_10, pub(crate) OsString); impl Rename { @@ -349,7 +347,7 @@ impl Debug for Rename { } } -/// Information for the [SyncFilter::renamed][crate::SyncFilter::renamed] callback. +/// Information for the [SyncFilter::renamed][crate::filter::SyncFilter::renamed] callback. pub struct Renamed(pub(crate) CF_CALLBACK_PARAMETERS_0_9, pub(crate) OsString); impl Renamed { @@ -370,7 +368,7 @@ impl Debug for Renamed { } /// The reason a placeholder has been dehydrated. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DehydrationReason { /// The user manually dehydrated the placeholder. UserManually, diff --git a/src/filter/sync_filter.rs b/src/filter/sync_filter.rs index 46a334a..2400ba5 100644 --- a/src/filter/sync_filter.rs +++ b/src/filter/sync_filter.rs @@ -8,7 +8,7 @@ use crate::{ /// Core functions for implementing a Sync Engine. /// -/// `Send` and `Sync` are required as the callback could be invoked from an arbitrary thread, [read +/// [Send] and [Sync] are required as the callback could be invoked from an arbitrary thread, [read /// here](https://docs.microsoft.com/en-us/windows/win32/api/cfapi/ne-cfapi-cf_callback_type#remarks). pub trait SyncFilter: Send + Sync { /// A placeholder hydration has been requested. This means that the placeholder should be @@ -103,7 +103,7 @@ pub trait SyncFilter: Send + Sync { /// However, it is up to **you** to approve this. Use the ticket to approve the /// request. /// - /// When the operation is completed, the [SyncFilter::renamed][crate::SyncFilter::renamed] callback will be called. + /// When the operation is completed, the [SyncFilter::renamed] callback will be called. fn rename( &self, _request: Request, diff --git a/src/filter/ticket.rs b/src/filter/ticket.rs index 972dc72..d9b3759 100644 --- a/src/filter/ticket.rs +++ b/src/filter/ticket.rs @@ -12,7 +12,7 @@ use crate::{ sealed, utility, }; -/// A ticket for the [SyncFilter::fetch_data][crate::SyncFilter::fetch_data] callback. +/// A ticket for the [SyncFilter::fetch_data][crate::filter::SyncFilter::fetch_data] callback. #[derive(Debug)] pub struct FetchData { connection_key: RawConnectionKey, @@ -20,7 +20,7 @@ pub struct FetchData { } impl FetchData { - /// Create a new [FetchData][crate::ticket::FetchData]. + /// Create a new [FetchData]. pub(crate) fn new(connection_key: RawConnectionKey, transfer_key: RawTransferKey) -> Self { Self { connection_key, @@ -79,7 +79,7 @@ impl utility::WriteAt for FetchData { impl sealed::Sealed for FetchData {} -/// A ticket for the [SyncFilter::validate_data][crate::SyncFilter::validate_data] callback. +/// A ticket for the [SyncFilter::validate_data][crate::filter::SyncFilter::validate_data] callback. #[derive(Debug)] pub struct ValidateData { connection_key: RawConnectionKey, @@ -87,7 +87,7 @@ pub struct ValidateData { } impl ValidateData { - /// Create a new [ValidateData][crate::ticket::ValidateData]. + /// Create a new [ValidateData]. pub(crate) fn new(connection_key: RawConnectionKey, transfer_key: RawTransferKey) -> Self { Self { connection_key, @@ -97,9 +97,6 @@ impl ValidateData { /// Validates the data range in the placeholder file is valid. /// - /// This method should be used in the - /// [SyncFilter::validate_data][crate::SyncFilter::validate_data] callback. - /// /// This method is equivalent to calling `CfExecute` with `CF_OPERATION_TYPE_ACK_DATA`. // TODO: make this generic over a RangeBounds // if the range specified is past the current file length, will it consider that range to be validated? @@ -129,7 +126,7 @@ impl utility::ReadAt for ValidateData { impl sealed::Sealed for ValidateData {} -/// A ticket for the [SyncFilter::fetch_placeholders][crate::SyncFilter::fetch_placeholders] callback. +/// A ticket for the [SyncFilter::fetch_placeholders][crate::filter::SyncFilter::fetch_placeholders] callback. #[derive(Debug)] pub struct FetchPlaceholders { connection_key: RawConnectionKey, @@ -137,7 +134,7 @@ pub struct FetchPlaceholders { } impl FetchPlaceholders { - /// Create a new [FetchPlaceholders][crate::ticket::FetchPlaceholders]. + /// Create a new [FetchPlaceholders]. pub(crate) fn new(connection_key: RawConnectionKey, transfer_key: RawTransferKey) -> Self { Self { connection_key, @@ -147,7 +144,7 @@ impl FetchPlaceholders { /// Creates a list of placeholder files/directorys on the file system. /// - /// The value returned is the final [Usn][crate::Usn] (and if they succeeded) after each placeholder is created. + /// The value returned is the final [Usn][crate::usn::Usn] (and if they succeeded) after each placeholder is created. pub fn pass_with_placeholder(&self, placeholders: &mut [PlaceholderFile]) -> core::Result<()> { command::CreatePlaceholders { total: placeholders.len() as _, @@ -157,7 +154,7 @@ impl FetchPlaceholders { } } -/// A ticket for the [SyncFilter::dehydrate][crate::SyncFilter::dehydrate] callback. +/// A ticket for the [SyncFilter::dehydrate][crate::filter::SyncFilter::dehydrate] callback. #[derive(Debug)] pub struct Dehydrate { connection_key: RawConnectionKey, @@ -165,7 +162,7 @@ pub struct Dehydrate { } impl Dehydrate { - /// Create a new [Dehydrate][crate::ticket::Dehydrate]. + /// Create a new [Dehydrate]. pub(crate) fn new(connection_key: RawConnectionKey, transfer_key: RawTransferKey) -> Self { Self { connection_key, @@ -184,7 +181,7 @@ impl Dehydrate { } } -/// A ticket for the [SyncFilter::delete][crate::SyncFilter::delete] callback. +/// A ticket for the [SyncFilter::delete][crate::filter::SyncFilter::delete] callback. #[derive(Debug)] pub struct Delete { connection_key: RawConnectionKey, @@ -192,7 +189,7 @@ pub struct Delete { } impl Delete { - /// Create a new [Delete][crate::ticket::Delete]. + /// Create a new [Delete]. pub(crate) fn new(connection_key: RawConnectionKey, transfer_key: RawTransferKey) -> Self { Self { connection_key, @@ -206,7 +203,7 @@ impl Delete { } } -/// A ticket for the [SyncFilter::rename][crate::SyncFilter::rename] callback. +/// A ticket for the [SyncFilter::rename][crate::filter::SyncFilter::rename] callback. #[derive(Debug)] pub struct Rename { connection_key: RawConnectionKey, @@ -214,7 +211,7 @@ pub struct Rename { } impl Rename { - /// Create a new [Rename][crate::ticket::Rename]. + /// Create a new [Rename]. pub(crate) fn new(connection_key: RawConnectionKey, transfer_key: RawTransferKey) -> Self { Self { connection_key, diff --git a/src/metadata.rs b/src/metadata.rs index 5f6bb0e..2dfae44 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -8,11 +8,12 @@ use windows::Win32::Storage::{ use crate::sealed; -/// The metadata for a [PlaceholderFile][crate::PlaceholderFile]. +/// The metadata for placeholder. #[derive(Debug, Clone, Copy, Default)] pub struct Metadata(pub(crate) CF_FS_METADATA); impl Metadata { + /// The default [Metadata] with `FILE_ATTRIBUTE_NORMAL` attribute. pub fn file() -> Self { Self(CF_FS_METADATA { BasicInfo: FILE_BASIC_INFO { @@ -23,6 +24,7 @@ impl Metadata { }) } + /// The default [Metadata] with `FILE_ATTRIBUTE_DIRECTORY` attribute. pub fn directory() -> Self { Self(CF_FS_METADATA { BasicInfo: FILE_BASIC_INFO { diff --git a/src/root/session.rs b/src/root/session.rs index 20e12c8..6c83615 100644 --- a/src/root/session.rs +++ b/src/root/session.rs @@ -48,19 +48,18 @@ impl Session { Self::default() } - // TODO: what specifically causes an implicit hydration? /// The [block_implicit_hydration][crate::Session::block_implicit_hydration] flag will prevent /// implicit placeholder hydrations from invoking - /// [SyncFilter::fetch_data][crate::SyncFilter::fetch_data]. This could occur when an + /// [SyncFilter::fetch_data][crate::filter::SyncFilter::fetch_data]. This could occur when an /// anti-virus is scanning file system activity on files within the sync root. /// - /// A call to the [FileExt::hydrate][crate::ext::FileExt::hydrate] trait will not be blocked by this flag. + /// A call to the [Placeholder::hydrate][crate::placeholder::Placeholder::hydrate] trait will not be blocked by this flag. pub fn block_implicit_hydration(mut self) -> Self { self.0 |= CloudFilters::CF_CONNECT_FLAG_BLOCK_SELF_IMPLICIT_HYDRATION; self } - /// Initiates a connection to the sync root with the given [SyncFilter][crate::SyncFilter]. + /// Initiates a connection to the sync root with the given [SyncFilter]. pub fn connect(self, path: P, filter: T) -> core::Result>> where P: AsRef, diff --git a/src/root/sync_root_id.rs b/src/root/sync_root_id.rs index 2684d29..220cded 100644 --- a/src/root/sync_root_id.rs +++ b/src/root/sync_root_id.rs @@ -1,6 +1,12 @@ -use std::{ffi::OsString, mem::MaybeUninit, os::windows::ffi::OsStringExt, path::Path, ptr}; +use std::{ + ffi::OsString, + mem::MaybeUninit, + os::windows::ffi::{OsStrExt, OsStringExt}, + path::Path, + ptr, +}; -use widestring::{U16CString, U16Str, U16String}; +use widestring::{U16CStr, U16Str, U16String}; use windows::{ core::{self, HSTRING, PWSTR}, Storage::Provider::{StorageProviderSyncRootInfo, StorageProviderSyncRootManager}, @@ -25,7 +31,7 @@ pub fn is_supported() -> core::Result { StorageProviderSyncRootManager::IsSupported() } -/// A builder to construct a [SyncRootId][crate::SyncRootId]. +/// A builder to construct a [SyncRootId]. #[derive(Debug, Clone)] pub struct SyncRootIdBuilder { provider_name: U16String, @@ -82,7 +88,7 @@ impl SyncRootIdBuilder { self } - /// Constructs a [SyncRootId][crate::SyncRootId] from the builder. + /// Constructs a [SyncRootId] from the builder. pub fn build(self) -> SyncRootId { SyncRootId(HSTRING::from_wide( &[ @@ -137,7 +143,7 @@ impl SyncRootId { StorageProviderSyncRootManager::Unregister(&self.0) } - /// A reference to the [SyncRootId] as a 16 bit string. + /// Encodes the [SyncRootId] to an [OsString]. pub fn to_os_string(&self) -> OsString { OsString::from_wide(self.0.as_wide()) } @@ -186,8 +192,19 @@ impl SecurityId { // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getcurrentthreadeffectivetoken const CURRENT_THREAD_EFFECTIVE_TOKEN: HANDLE = HANDLE(-6); - /// Creates a new [SecurityId] without any assertions. + /// Creates a new [SecurityId] from [OsString]. + /// + /// # Panics + /// + /// Panics if the security id contains an exclamation point. pub fn new(id: OsString) -> Self { + assert!( + !id.as_os_str() + .encode_wide() + .any(|x| x == SyncRootId::SEPARATOR), + "security id cannot contain exclamation points" + ); + Self(id.into()) } @@ -222,10 +239,10 @@ impl SecurityId { let mut sid = PWSTR(ptr::null_mut()); ConvertSidToStringSidW(token.User.Sid, &mut sid as *mut _).ok()?; - let string_sid = U16CString::from_ptr_str(sid.0).into_ustring(); + let string_sid = U16CStr::from_ptr_str(sid.0).to_os_string(); LocalFree(sid.0 as isize); - Ok(SecurityId::new(string_sid.to_os_string())) + Ok(SecurityId::new(string_sid)) } } } From b794e585d83d2cb8cba31ac1b296369e658c3e39 Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Mon, 1 Jul 2024 18:51:41 +0800 Subject: [PATCH 14/34] refactor: bump dependencies --- Cargo.toml | 5 ++-- src/ext/file.rs | 5 ++-- src/ext/path.rs | 2 +- src/placeholder.rs | 45 ++++++++++++-------------------- src/placeholder_file.rs | 16 +++++------- src/request.rs | 2 +- src/root/register.rs | 37 +++++++++++++-------------- src/root/session.rs | 55 +++++++++++++++++++++++----------------- src/root/sync_root_id.rs | 37 +++++++++++++-------------- src/usn.rs | 2 +- src/utility.rs | 6 ++++- 11 files changed, 103 insertions(+), 109 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cb4a155..961df4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,8 @@ edition = "2021" [dependencies] widestring = "1.0.2" nt-time = "0.8.0" -memoffset = "0.6.4" -windows = { version = "0.33.0", features = [ - "alloc", +memoffset = "0.9.1" +windows = { version = "0.52.0", features = [ "Win32_Foundation", "Win32_Storage_CloudFilters", "Win32_System_SystemServices", diff --git a/src/ext/file.rs b/src/ext/file.rs index 561ac1b..ae48a50 100644 --- a/src/ext/file.rs +++ b/src/ext/file.rs @@ -3,7 +3,6 @@ use std::{ mem, ops::{Bound, RangeBounds}, os::windows::{io::AsRawHandle, prelude::RawHandle}, - ptr, }; use widestring::U16CStr; @@ -53,7 +52,7 @@ pub trait FileExt: AsRawHandle + Sealed { CloudFilters::CF_SYNC_ROOT_INFO_STANDARD, data.as_mut_ptr() as *mut _, data.len() as u32, - ptr::null_mut(), + None, )?; } @@ -100,7 +99,7 @@ fn dehydrate>( } else { CloudFilters::CF_DEHYDRATE_FLAG_BACKGROUND }, - ptr::null_mut(), + None, ) } } diff --git a/src/ext/path.rs b/src/ext/path.rs index 2107162..31382c0 100644 --- a/src/ext/path.rs +++ b/src/ext/path.rs @@ -25,7 +25,7 @@ where /// Information about the sync root that the path is located in. fn sync_root_info(&self) -> core::Result { StorageProviderSyncRootManager::GetSyncRootInformationForFolder( - StorageFolder::GetFolderFromPathAsync( + &StorageFolder::GetFolderFromPathAsync( &U16String::from_os_str(self.as_ref().as_os_str()).to_hstring(), )? .get()?, diff --git a/src/placeholder.rs b/src/placeholder.rs index abf19b1..837d17f 100644 --- a/src/placeholder.rs +++ b/src/placeholder.rs @@ -86,7 +86,7 @@ impl Drop for OwnedPlaceholderHandle { match self.handle_type { PlaceholderHandleType::CfApi => unsafe { CfCloseHandle(self.handle) }, PlaceholderHandleType::Win32 => unsafe { - CloseHandle(self.handle); + _ = CloseHandle(self.handle); }, } } @@ -642,8 +642,7 @@ impl Placeholder { false => CloudFilters::CF_IN_SYNC_STATE_NOT_IN_SYNC, }, CloudFilters::CF_SET_IN_SYNC_FLAG_NONE, - usn.into() - .map_or(ptr::null_mut(), |x| ptr::read(x) as *mut _), + usn.into().map(|x| ptr::read(x) as *mut _), ) }?; @@ -656,7 +655,7 @@ impl Placeholder { /// [CfSetPinState](https://learn.microsoft.com/en-us/windows/win32/api/cfapi/nf-cfapi-cfsetpinstate), /// [What does "Pinned" Mean?](https://www.userfilesystem.com/programming/faq/#nav_howdoesthealwayskeeponthisdevicemenuworks) pub fn mark_pin(&mut self, state: PinState, options: PinOptions) -> core::Result<&mut Self> { - unsafe { CfSetPinState(self.handle.handle, state.into(), options.0, ptr::null_mut()) }?; + unsafe { CfSetPinState(self.handle.handle, state.into(), options.0, None) }?; Ok(self) } @@ -674,15 +673,11 @@ impl Placeholder { unsafe { CfConvertToPlaceholder( self.handle.handle, - match options.blob.is_empty() { - true => ptr::null(), - false => options.blob.as_ptr() as *const _, - }, + (!options.blob.is_empty()).then_some(options.blob.as_ptr() as *const _), options.blob.len() as _, options.flags, - usn.into() - .map_or(ptr::null_mut(), |x| ptr::read(x) as *mut _), - ptr::null_mut(), + usn.into().map(|x| ptr::read(x) as *mut _), + None, ) }?; @@ -703,7 +698,7 @@ impl Placeholder { CloudFilters::CF_PLACEHOLDER_INFO_STANDARD, data.as_mut_ptr() as *mut _, data.len() as u32, - ptr::null_mut(), + None, ) }; @@ -716,7 +711,7 @@ impl Placeholder { .1[0] as *const _, data, })), - Err(e) if e.win32_error() == Some(ERROR_NOT_A_CLOUD_FILE) => Ok(None), + Err(e) if e.code() == ERROR_NOT_A_CLOUD_FILE.to_hresult() => Ok(None), Err(e) => Err(e), } } @@ -732,21 +727,13 @@ impl Placeholder { unsafe { CfUpdatePlaceholder( self.handle.handle, - options.metadata.map_or(ptr::null(), |x| &x.0 as *const _), - match options.blob.is_empty() { - true => ptr::null(), - false => options.blob.as_ptr() as *const _, - }, + options.metadata.map(|x| &x.0 as *const _), + (!options.blob.is_empty()).then_some(options.blob.as_ptr() as *const _), options.blob.len() as _, - match options.dehydrate_ranges.is_empty() { - true => ptr::null(), - false => options.dehydrate_ranges.as_ptr(), - }, - options.dehydrate_ranges.len() as u32, + (options.dehydrate_ranges.is_empty()).then_some(&options.dehydrate_ranges), options.flags, - usn.into() - .map_or(ptr::null_mut(), |x| ptr::read(x) as *mut _), - ptr::null_mut(), + usn.into().map(|u| u as *mut _), + None, ) }?; @@ -769,7 +756,7 @@ impl Placeholder { buffer.len() as i64, buffer as *mut _ as *mut _, buffer.len() as u32, - length.assume_init_mut(), + Some(length.as_mut_ptr()), ) .map(|_| length.assume_init()) } @@ -854,7 +841,7 @@ impl Placeholder { Bound::Unbounded => -1, }, CloudFilters::CF_HYDRATE_FLAG_NONE, - ptr::null_mut(), + None, ) } } @@ -885,7 +872,7 @@ impl TryFrom for File { CfRevertPlaceholder( placeholder.handle.handle, CloudFilters::CF_REVERT_FLAG_NONE, - ptr::null_mut(), + None, ) } .map(|_| unsafe { File::from_raw_handle(mem::transmute(placeholder.handle.handle)) }), diff --git a/src/placeholder_file.rs b/src/placeholder_file.rs index f97527a..4c4a352 100644 --- a/src/placeholder_file.rs +++ b/src/placeholder_file.rs @@ -114,14 +114,13 @@ impl PlaceholderFile { /// /// If you need to create placeholders from the [SyncFilter::fetch_placeholders][crate::SyncFilter::fetch_placeholders] callback, do not use this method. Instead, use /// [FetchPlaceholders::pass_with_placeholders][crate::ticket::FetchPlaceholders::pass_with_placeholders]. - pub fn create>(mut self, parent: impl AsRef) -> core::Result { + pub fn create>(self, parent: impl AsRef) -> core::Result { unsafe { CfCreatePlaceholders( - parent.as_ref().as_os_str(), - &mut self as *mut _ as *mut _, - 1, + PCWSTR(U16CString::from_os_str(parent.as_ref()).unwrap().as_ptr()), + &mut [self.0], CloudFilters::CF_CREATE_FLAG_NONE, - ptr::null_mut(), + None, )?; } @@ -155,11 +154,10 @@ impl BatchCreate for [PlaceholderFile] { fn create>(&mut self, path: P) -> core::Result<()> { unsafe { CfCreatePlaceholders( - path.as_ref().as_os_str(), - self.as_mut_ptr() as *mut CF_PLACEHOLDER_CREATE_INFO, - self.len() as u32, + PCWSTR(U16CString::from_os_str(path.as_ref()).unwrap().as_ptr()), + slice::from_raw_parts_mut(self.as_mut_ptr() as *mut _, self.len()), CloudFilters::CF_CREATE_FLAG_NONE, - ptr::null_mut(), + None, ) } } diff --git a/src/request.rs b/src/request.rs index 59d55a6..9f9b108 100644 --- a/src/request.rs +++ b/src/request.rs @@ -3,7 +3,7 @@ use std::{ffi::OsString, path::PathBuf, slice}; use widestring::{u16cstr, U16CStr}; use windows::Win32::Storage::CloudFilters::{CF_CALLBACK_INFO, CF_PROCESS_INFO}; -pub type RawConnectionKey = isize; +pub type RawConnectionKey = i64; pub type RawTransferKey = i64; /// A struct containing various information for the current file operation. diff --git a/src/root/register.rs b/src/root/register.rs index 8a47ad4..2c00e8b 100644 --- a/src/root/register.rs +++ b/src/root/register.rs @@ -15,9 +15,8 @@ use windows::{ Streams::DataWriter, }, Win32::Storage::CloudFilters::{ - self, CF_HYDRATION_POLICY_MODIFIER_USHORT, CF_HYDRATION_POLICY_PRIMARY, - CF_HYDRATION_POLICY_PRIMARY_USHORT, CF_INSYNC_POLICY, CF_POPULATION_POLICY_PRIMARY, - CF_POPULATION_POLICY_PRIMARY_USHORT, + self, CF_HYDRATION_POLICY_MODIFIER, CF_HYDRATION_POLICY_PRIMARY, CF_INSYNC_POLICY, + CF_POPULATION_POLICY_PRIMARY, }, }; @@ -158,10 +157,10 @@ impl<'a> Registration<'a> { info.SetHydrationPolicyModifier(self.hydration_policy.0)?; info.SetPopulationPolicy(self.population_type.into())?; info.SetInSyncPolicy(self.supported_attributes.0)?; - info.SetDisplayNameResource(self.display_name.to_hstring())?; - info.SetIconResource(self.icon.to_hstring())?; + info.SetDisplayNameResource(&self.display_name.to_hstring())?; + info.SetIconResource(&self.icon.to_hstring())?; info.SetPath( - StorageFolder::GetFolderFromPathAsync( + &StorageFolder::GetFolderFromPathAsync( &U16String::from_os_str(path.as_ref().as_os_str()).to_hstring(), )? .get()?, @@ -177,20 +176,20 @@ impl<'a> Registration<'a> { info.SetProviderId(provider_id)?; } if let Some(version) = &self.version { - info.SetVersion(version.to_hstring())?; + info.SetVersion(&version.to_hstring())?; } if let Some(uri) = &self.recycle_bin_uri { - info.SetRecycleBinUri(Uri::CreateUri(uri.to_hstring())?)?; + info.SetRecycleBinUri(&Uri::CreateUri(&uri.to_hstring())?)?; } if let Some(blob) = &self.blob { // TODO: implement IBuffer interface for slices to avoid a copy let writer = DataWriter::new()?; writer.WriteBytes(blob)?; - info.SetContext(writer.DetachBuffer()?)?; + info.SetContext(&writer.DetachBuffer()?)?; } - StorageProviderSyncRootManager::Register(info) + StorageProviderSyncRootManager::Register(&info) } } @@ -228,9 +227,9 @@ impl From for StorageProviderHydrationPolicy { } } -impl From for HydrationType { - fn from(primary: CF_HYDRATION_POLICY_PRIMARY_USHORT) -> Self { - match CF_HYDRATION_POLICY_PRIMARY(primary.us) { +impl From for HydrationType { + fn from(primary: CF_HYDRATION_POLICY_PRIMARY) -> Self { + match primary { CloudFilters::CF_HYDRATION_POLICY_PARTIAL => HydrationType::Partial, CloudFilters::CF_HYDRATION_POLICY_PROGRESSIVE => HydrationType::Progressive, CloudFilters::CF_HYDRATION_POLICY_FULL => HydrationType::Full, @@ -278,9 +277,9 @@ impl Default for HydrationPolicy { } } -impl From for HydrationPolicy { - fn from(primary: CF_HYDRATION_POLICY_MODIFIER_USHORT) -> Self { - Self(StorageProviderHydrationPolicyModifier(primary.us as u32)) +impl From for HydrationPolicy { + fn from(primary: CF_HYDRATION_POLICY_MODIFIER) -> Self { + Self(StorageProviderHydrationPolicyModifier(primary.0 as u32)) } } @@ -299,9 +298,9 @@ impl From for StorageProviderPopulationPolicy { } } -impl From for PopulationType { - fn from(primary: CF_POPULATION_POLICY_PRIMARY_USHORT) -> Self { - match CF_POPULATION_POLICY_PRIMARY(primary.us) { +impl From for PopulationType { + fn from(primary: CF_POPULATION_POLICY_PRIMARY) -> Self { + match primary { CloudFilters::CF_POPULATION_POLICY_FULL => PopulationType::Full, CloudFilters::CF_POPULATION_POLICY_ALWAYS_FULL => PopulationType::AlwaysFull, _ => unreachable!(), diff --git a/src/root/session.rs b/src/root/session.rs index 6c83615..2e116fd 100644 --- a/src/root/session.rs +++ b/src/root/session.rs @@ -4,7 +4,6 @@ use std::{ mem::{self, MaybeUninit}, os::windows::{fs::OpenOptionsExt, io::AsRawHandle}, path::{Path, PathBuf}, - ptr, sync::{ mpsc::{self, Sender, TryRecvError}, Arc, Weak, @@ -13,11 +12,11 @@ use std::{ time::Duration, }; -use widestring::U16Str; +use widestring::{u16cstr, U16CString, U16Str}; use windows::{ - core, + core::{self, PCWSTR}, Win32::{ - Foundation::{ERROR_IO_INCOMPLETE, HANDLE}, + Foundation::{ERROR_IO_INCOMPLETE, HANDLE, WIN32_ERROR}, Storage::{ CloudFilters::{self, CfConnectSyncRoot, CF_CONNECT_FLAGS}, FileSystem::{ @@ -27,7 +26,7 @@ use windows::{ }, System::{ Com::{self, CoCreateInstance}, - Search::{self, ISearchCatalogManager, ISearchManager}, + Search::{self, ISearchManager}, IO::{CancelIoEx, GetOverlappedResult}, }, }, @@ -72,11 +71,15 @@ impl Session { let callbacks = filter::callbacks::(); let key = unsafe { CfConnectSyncRoot( - path.as_ref().as_os_str(), + PCWSTR( + U16CString::from_os_str(path.as_ref()) + .expect("not contains nul") + .as_ptr(), + ), callbacks.as_ptr(), // create a weak arc so that it could be upgraded when it's being used and when the // connection is closed, the filter could be freed - Weak::into_raw(Arc::downgrade(&filter)) as *const _, + Some(Weak::into_raw(Arc::downgrade(&filter)) as *const _), // This is enabled by default to remove the Option requirement around various fields of the // [Request][crate::Request] struct self.0 @@ -112,13 +115,22 @@ fn index_path(path: &Path) -> core::Result<()> { Com::CLSCTX_SERVER, )?; - let catalog: ISearchCatalogManager = searcher.GetCatalog("SystemIndex")?; + let catalog = searcher.GetCatalog(PCWSTR(u16cstr!("SystemIndex").as_ptr()))?; let mut url = OsString::from("file:///"); url.push(path); let crawler = catalog.GetCrawlScopeManager()?; - crawler.AddDefaultScopeRule(url, true, Search::FF_INDEXCOMPLEXURLS.0 as u32)?; + crawler.AddDefaultScopeRule( + PCWSTR( + U16CString::from_os_str(url) + .expect("not contains nul") + .as_ptr(), + ), + true, + Search::FF_INDEXCOMPLEXURLS.0 as u32, + )?; + crawler.SaveAll() } } @@ -148,38 +160,35 @@ fn spawn_root_watcher( CHANGE_BUF_SIZE as _, true, FILE_NOTIFY_CHANGE_ATTRIBUTES, - ptr::null_mut(), - overlapped.as_mut_ptr(), + None, + Some(overlapped.as_mut_ptr()), None, ) } - .ok() .expect("read directory changes"); loop { - if unsafe { - !GetOverlappedResult( + if let Err(e) = unsafe { + GetOverlappedResult( HANDLE(sync_root.as_raw_handle() as _), - overlapped.as_mut_ptr(), + overlapped.as_ptr(), transferred.as_mut_ptr(), false, ) - } - .into() - { - let win32_err = core::Error::from_win32().win32_error(); - if win32_err != Some(ERROR_IO_INCOMPLETE) { + } { + if e.code() != ERROR_IO_INCOMPLETE.to_hresult() { panic!( - "get overlapped result: {win32_err:?}, expected: {ERROR_IO_INCOMPLETE:?}" + "get overlapped result: {:?}, expected: {ERROR_IO_INCOMPLETE:?}", + WIN32_ERROR::from_error(&e), ); } // cancel by user if !matches!(rx.try_recv(), Err(TryRecvError::Empty)) { - unsafe { + _ = unsafe { CancelIoEx( HANDLE(sync_root.as_raw_handle() as _), - overlapped.as_mut_ptr(), + Some(overlapped.as_ptr()), ) }; return; diff --git a/src/root/sync_root_id.rs b/src/root/sync_root_id.rs index 220cded..c3ab596 100644 --- a/src/root/sync_root_id.rs +++ b/src/root/sync_root_id.rs @@ -11,14 +11,13 @@ use windows::{ core::{self, HSTRING, PWSTR}, Storage::Provider::{StorageProviderSyncRootInfo, StorageProviderSyncRootManager}, Win32::{ - Foundation::{self, GetLastError, HANDLE}, + Foundation::{self, LocalFree, ERROR_INSUFFICIENT_BUFFER, HANDLE, HLOCAL}, Security::{self, Authorization::ConvertSidToStringSidW, GetTokenInformation, TOKEN_USER}, Storage::CloudFilters, - System::Memory::LocalFree, }, }; -use crate::ext::PathExt; +use crate::{ext::PathExt, utility::ToHString}; /// Returns a list of active sync roots. pub fn active_roots() -> core::Result> { @@ -90,14 +89,15 @@ impl SyncRootIdBuilder { /// Constructs a [SyncRootId] from the builder. pub fn build(self) -> SyncRootId { - SyncRootId(HSTRING::from_wide( - &[ + SyncRootId( + [ self.provider_name.as_slice(), self.user_security_id.0.as_slice(), self.account_name.as_slice(), ] - .join(&SyncRootId::SEPARATOR), - )) + .join(&SyncRootId::SEPARATOR) + .to_hstring(), + ) } } @@ -128,7 +128,7 @@ impl SyncRootId { Ok( match StorageProviderSyncRootManager::GetSyncRootInformationForId(&self.0) { Ok(_) => true, - Err(err) => err.win32_error() != Some(Foundation::ERROR_NOT_FOUND), + Err(err) => err.code() != Foundation::ERROR_NOT_FOUND.to_hresult(), }, ) } @@ -215,32 +215,31 @@ impl SecurityId { let mut token = MaybeUninit::::uninit(); // get the token size - if !GetTokenInformation( + if let Err(e) = GetTokenInformation( Self::CURRENT_THREAD_EFFECTIVE_TOKEN, Security::TokenUser, - ptr::null_mut(), + None, 0, &mut token_size, - ) - .as_bool() - && GetLastError() == Foundation::ERROR_INSUFFICIENT_BUFFER - { + ) { + if e.code() != ERROR_INSUFFICIENT_BUFFER.to_hresult() { + Err(e)?; + } GetTokenInformation( Self::CURRENT_THREAD_EFFECTIVE_TOKEN, Security::TokenUser, - &mut token as *mut _ as *mut _, + Some(&mut token as *mut _ as *mut _), token_size, &mut token_size, - ) - .ok()?; + )?; } let token = token.assume_init(); let mut sid = PWSTR(ptr::null_mut()); - ConvertSidToStringSidW(token.User.Sid, &mut sid as *mut _).ok()?; + ConvertSidToStringSidW(token.User.Sid, &mut sid as *mut _)?; let string_sid = U16CStr::from_ptr_str(sid.0).to_os_string(); - LocalFree(sid.0 as isize); + _ = LocalFree(HLOCAL(sid.0 as *mut _)); Ok(SecurityId::new(string_sid)) } diff --git a/src/usn.rs b/src/usn.rs index f9c459d..082a9a0 100644 --- a/src/usn.rs +++ b/src/usn.rs @@ -9,4 +9,4 @@ /// instance, [FileExt::update][crate::ext::FileExt::update] will not apply the specified changes /// unless if the passed USN matches the most recent USN of the file. This avoids applying changes /// that may be out of date. -pub type Usn = u64; +pub type Usn = i64; diff --git a/src/utility.rs b/src/utility.rs index 8dfcaeb..e56ed70 100644 --- a/src/utility.rs +++ b/src/utility.rs @@ -11,8 +11,12 @@ where Self: AsRef<[u16]>, { /// Converts a 16-bit buffer to a Windows reference-counted [HSTRING][windows::core::HSTRING]. + /// + /// # Panics + /// + /// Panics if [HeapAlloc](https://docs.microsoft.com/en-us/windows/win32/api/heapapi/nf-heapapi-heapalloc) fails. fn to_hstring(&self) -> HSTRING { - HSTRING::from_wide(self.as_ref()) + HSTRING::from_wide(self.as_ref()).unwrap() } } From b7467c5ad18fa040b1842ccce05b50cd20194ab7 Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Tue, 2 Jul 2024 15:57:37 +0800 Subject: [PATCH 15/34] fix: SyncRootId::to_components --- src/root/sync_root_id.rs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/root/sync_root_id.rs b/src/root/sync_root_id.rs index c3ab596..86a4491 100644 --- a/src/root/sync_root_id.rs +++ b/src/root/sync_root_id.rs @@ -162,22 +162,21 @@ impl SyncRootId { /// /// The order goes as follows: /// `(provider-id, security-id, account-name)` - // TODO: This doesn't work properly, it forgets to include the account name + /// + /// # Panics + /// + /// Panics if the sync root id does not have exactly three components. pub fn to_components(&self) -> (&U16Str, &U16Str, &U16Str) { let mut components = Vec::with_capacity(3); - let mut bytes = self.0.as_wide(); + components.extend( + self.0 + .as_wide() + .split(|&byte| byte == Self::SEPARATOR) + .map(U16Str::from_slice), + ); - for index in 0..2 { - match bytes.iter().position(|&byte| byte == Self::SEPARATOR) { - Some(position) => { - components.insert(index, U16Str::from_slice(bytes)); - bytes = &bytes[(position + 1)..]; - } - None => { - // TODO: return a result instead of panic - panic!("malformed sync root id, got {:?}", components) - } - } + if components.len() != 3 { + panic!("malformed sync root id, got {:?}", components) } (components[0], components[1], components[2]) From 534909309fbac48c9c82d86ac5a1024fd9aad016 Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Tue, 2 Jul 2024 16:10:11 +0800 Subject: [PATCH 16/34] rename to cloud-filter --- Cargo.toml | 10 ++++++++-- README.md | 37 ++++++++++++++++++++----------------- examples/sftp/Cargo.toml | 2 +- examples/sftp/src/main.rs | 10 +++++----- 4 files changed, 34 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 961df4c..932e0f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,13 @@ [package] -name = "wincs" -version = "0.1.0" +name = "cloud-filter" +version = "0.0.1" +authors = ["ok-nick ", "ho-229 "] edition = "2021" +description = "A safe and idiomatic wrapper around the Windows Cloud Filter API" +license = "MIT" +repository = "https://github.com/ho-229/cloud-filter" +documentation = "https://docs.rs/cloud-filter" +exclude = ["examples/"] [dependencies] widestring = "1.0.2" diff --git a/README.md b/README.md index d22fd0a..d10e592 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,17 @@
-

wincs

+

cloud-filter

Windows Cloud Sync

- docs - crates - discord + docs + crates

-> **Warning** -> Read more about the future of `wincs` in [issue #8](https://github.com/ok-nick/wincs/issues/8). +> This is a fork of [wincs](https://github.com/ok-nick/wincs), thanks for [ok-nick](https://github.com/ok-nick)'s great work. -`wincs` is a safe and idiomatic wrapper around the native [Windows Cloud Filter API](https://docs.microsoft.com/en-us/windows/win32/cfapi/build-a-cloud-file-sync-engine). The Cloud Filter API enables developers to implement their own remote file system from within user space. It is much like [FUSE](#why-not-fuse), although it contains many first-class Windows features that are only available through its API. +`cloud-filter` is a safe and idiomatic wrapper around the native [Windows Cloud Filter API](https://docs.microsoft.com/en-us/windows/win32/cfapi/build-a-cloud-file-sync-engine). The Cloud Filter API enables developers to implement their own remote file system from within user space. It is much like [FUSE](#why-not-fuse), although it contains many first-class Windows features that are only available through its API. For example: + * [Placeholder files](#what-are-placeholders) * Partial files * Full files @@ -41,10 +40,8 @@ For example: As of right now, the Cloud Filter API is used in production by OneDrive, Google Drive, Dropbox, and many other clients. ## Examples -Below is a simple snippet of implementing a sync engine. For more, in-depth examples, please check out the [examples directory](https://github.com/ok-nick/wincs/tree/main/examples). -```rs -// TODO -``` + +Below is a simple snippet of implementing a sync engine. For more, in-depth examples, please check out the [examples directory](https://github.com/ho-229/cloud-filter/tree/main/examples). ## FAQ @@ -52,22 +49,28 @@ Below is a simple snippet of implementing a sync engine. For more, in-depth exam Unfortunately, FUSE is only implemented for Unix-like operating systems. Luckily, there are numerous alternatives for implementing file systems on Windows, such as `dokany` or `winfsp`. #### Why not `dokany`? -`dokany` has a Rust API and is accessible using safe code. However, it is fairly low-level and does not have the first-class capabilities supported by `wincs`. Read more [here](#wincs). + +`dokany` has a Rust API and is accessible using safe code. However, it is fairly low-level and does not have the first-class capabilities supported by `cloud-filter`. #### Why not `winfsp`? -Unlike `dokany`, `winfsp` currently does not have a Rust API. Perhaps at some point it may, but even so, it is impossible to have the first-class features supported by `wincs`. Read more [here](#wincs). + +Unlike `dokany`, `winfsp` currently does not have a Rust API. Perhaps at some point it may, but even so, it is impossible to have the first-class features supported by `cloud-filter`. ### What are placeholders? + Placeholders are internally [NTFS sparse files](https://docs.microsoft.com/en-us/windows/win32/fileio/sparse-files) and some [reparse point magic](https://docs.microsoft.com/en-us/windows/win32/cfapi/build-a-cloud-file-sync-engine#compatibility-with-applications-that-use-reparse-points). To put it simple, they are empty files that are meant to represent real files, although are not backed by any allocation unless requested. The way they work is heavily dependent on the sync engines' configuration. Know that if a process were to read the content of the placeholder, it would be "hydrated" (its file contents would be allocated). For more information, read [here](https://docs.microsoft.com/en-us/windows/win32/cfapi/build-a-cloud-file-sync-engine). -### I know `wincs` is maintained, but does Microsoft maintain the Cloud Filter API? -Of course, it is used by Microsoft's very own OneDrive Client. I have reported numerous issues and received quick feedback via the [Microsoft Q&A](https://docs.microsoft.com/en-us/answers/search.html?c=7&includeChildren=false&type=question&redirect=search%2Fsearch&sort=newest&q=cfapi). There are a lot of undocumented and unimplemented portions of the API, although they are not necessary for the features described [here](#wincs). +### I know `cloud-filter` is maintained, but does Microsoft maintain the Cloud Filter API? + +Of course, it is used by Microsoft's very own OneDrive Client. I have reported numerous issues and received quick feedback via the [Microsoft Q&A](https://docs.microsoft.com/en-us/answers/search.html?c=7&includeChildren=false&type=question&redirect=search%2Fsearch&sort=newest&q=cfapi). There are a lot of undocumented and unimplemented portions of the API, although they are not necessary for the features described. + +### Why is `cloud-filter` only for remote files? -### Why is `wincs` only for remote files? You are more than welcome to use it for local files, although the extra features may not suit your needs. It is recommended to instead use [ProjFS](https://docs.microsoft.com/en-us/windows/win32/projfs/projected-file-system), of which is also backed by Microsoft, but dedicated to "high-speed backing data stores." ## Additional Resources -If you are looking to contribute or want a deeper understanding of `wincs`, be sure to check out these resources: + +If you are looking to contribute or want a deeper understanding of `cloud-filter`, be sure to check out these resources: * [Build a Cloud Sync Engine that Supports Placeholder Files](https://docs.microsoft.com/en-us/windows/win32/cfapi/build-a-cloud-file-sync-engine) * [Integrate a Cloud Storage Provider](https://docs.microsoft.com/en-us/windows/win32/shell/integrate-cloud-storage) * [Microsoft Q&A Containing "cfapi"](https://docs.microsoft.com/en-us/answers/search.html?c=7&includeChildren=false&type=question&redirect=search%2Fsearch&sort=newest&q=cfapi) diff --git a/examples/sftp/Cargo.toml b/examples/sftp/Cargo.toml index 40d9d4b..34c51c9 100644 --- a/examples/sftp/Cargo.toml +++ b/examples/sftp/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -wincs = { path = "../../" } +cloud-filter = { path = "../../" } widestring = "1.0.2" ssh2 = "0.9.4" thiserror = "1.0.30" diff --git a/examples/sftp/src/main.rs b/examples/sftp/src/main.rs index f55a353..2f2bf52 100644 --- a/examples/sftp/src/main.rs +++ b/examples/sftp/src/main.rs @@ -8,11 +8,7 @@ use std::{ sync::mpsc, }; -use rkyv::{Archive, Deserialize, Serialize}; -use ssh2::Sftp; -use thiserror::Error; -use widestring::{u16str, U16String}; -use wincs::{ +use cloud_filter::{ error::{CResult, CloudErrorKind}, filter::{info, ticket, SyncFilter}, metadata::Metadata, @@ -22,6 +18,10 @@ use wincs::{ root::{HydrationType, PopulationType, Registration, SecurityId, Session, SyncRootIdBuilder}, utility::{FileTime, WriteAt}, }; +use rkyv::{Archive, Deserialize, Serialize}; +use ssh2::Sftp; +use thiserror::Error; +use widestring::{u16str, U16String}; // max should be 65536, this is done both in term-scp and sshfs because it's the // max packet size for a tcp connection From b8cf57b7dc4c5921588a66f07b6a1f5bb28f97a8 Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Tue, 2 Jul 2024 16:28:15 +0800 Subject: [PATCH 17/34] docs --- Cargo.toml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 932e0f4..6ea78da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ authors = ["ok-nick ", "ho-229 " edition = "2021" description = "A safe and idiomatic wrapper around the Windows Cloud Filter API" license = "MIT" -repository = "https://github.com/ho-229/cloud-filter" +repository = "https://github.com/ho-229/cloud-filter-rs" documentation = "https://docs.rs/cloud-filter" exclude = ["examples/"] diff --git a/README.md b/README.md index d10e592..c278fc9 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

cloud-filter

Windows Cloud Sync

- docs + docs crates

From b8db3da35279a4c7b71d4c58b059779bb2a6478d Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Fri, 5 Jul 2024 19:38:44 +0800 Subject: [PATCH 18/34] fix: segfault --- src/root/session.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/root/session.rs b/src/root/session.rs index 2e116fd..e440d35 100644 --- a/src/root/session.rs +++ b/src/root/session.rs @@ -204,7 +204,7 @@ fn spawn_root_watcher( let mut changes = Vec::with_capacity(8); let mut entry = changes_buf.as_ptr() as *const FILE_NOTIFY_INFORMATION; - while !entry.is_null() { + loop { let relative = unsafe { U16Str::from_ptr( &(*entry).FileName as *const _, @@ -213,7 +213,11 @@ fn spawn_root_watcher( }; changes.push(path.join(relative.to_os_string())); - entry = (unsafe { *entry }).NextEntryOffset as *const _; + + if unsafe { *entry }.NextEntryOffset == 0 { + break; + } + entry = unsafe { entry.byte_add((*entry).NextEntryOffset as _) }; } filter.state_changed(changes); From 6b00b57d3908cb22981243067a0a3f6217c359dd Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Fri, 5 Jul 2024 19:40:04 +0800 Subject: [PATCH 19/34] released 0.0.2 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6ea78da..84d1a8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cloud-filter" -version = "0.0.1" +version = "0.0.2" authors = ["ok-nick ", "ho-229 "] edition = "2021" description = "A safe and idiomatic wrapper around the Windows Cloud Filter API" From 02418919599b62e8dcbf51ee038a6a717b8a39c6 Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Mon, 8 Jul 2024 21:29:17 +0800 Subject: [PATCH 20/34] refactor: Placeholder::info --- src/placeholder.rs | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/src/placeholder.rs b/src/placeholder.rs index 837d17f..be63652 100644 --- a/src/placeholder.rs +++ b/src/placeholder.rs @@ -684,12 +684,49 @@ impl Placeholder { Ok(self) } + /// Gets various characteristics of the placeholder. + /// + /// Returns [None] if the handle not points to a placeholder. + /// + /// If the placeholder blob size is known, use [fixed_size_info](Self::fixed_size_info) instead. + pub fn info(&self) -> core::Result> { + let mut info_size = 0; + let mut data = vec![0u8; mem::size_of::() + 4096]; + let r = unsafe { + CfGetPlaceholderInfo( + self.handle.handle, + CloudFilters::CF_PLACEHOLDER_INFO_STANDARD, + data.as_mut_ptr() as *mut _, + data.len() as _, + Some(&mut info_size), + ) + }; + + match r { + Ok(()) => { + unsafe { data.set_len(info_size as _) }; + data.shrink_to_fit(); + + Ok(Some(PlaceholderInfo { + info: &unsafe { + data[..=mem::size_of::()] + .align_to::() + } + .1[0] as *const _, + data, + })) + } + Err(e) if e.code() == ERROR_NOT_A_CLOUD_FILE.to_hresult() => Ok(None), + Err(e) => Err(e), + } + } + /// Gets various characteristics of the placeholder. /// /// If the `blob_size` not matches the actual size of the blob, /// the call will returns `HRESULT_FROM_WIN32(ERROR_MORE_DATA)`. - /// Returns `None` if the handle not points to a placeholder. - pub fn info(&self, blob_size: usize) -> core::Result> { + /// Returns [None] if the handle not points to a placeholder. + pub fn fixed_size_info(&self, blob_size: usize) -> core::Result> { let mut data = vec![0; mem::size_of::() + blob_size]; let r = unsafe { From 493d29df0573dfb66016224c4fbc61bfc4b9317e Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Wed, 10 Jul 2024 19:45:27 +0800 Subject: [PATCH 21/34] refactor: SyncRootInfo --- Cargo.toml | 1 + examples/sftp/src/main.rs | 32 +-- src/ext/file.rs | 73 ++---- src/root/mod.rs | 10 +- src/root/register.rs | 375 ----------------------------- src/root/sync_root_id.rs | 53 +++-- src/root/sync_root_info.rs | 473 +++++++++++++++++++++++++++++++++++++ 7 files changed, 545 insertions(+), 472 deletions(-) delete mode 100644 src/root/register.rs create mode 100644 src/root/sync_root_info.rs diff --git a/Cargo.toml b/Cargo.toml index 84d1a8e..41b1092 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ documentation = "https://docs.rs/cloud-filter" exclude = ["examples/"] [dependencies] +flagset = "0.4.5" widestring = "1.0.2" nt-time = "0.8.0" memoffset = "0.9.1" diff --git a/examples/sftp/src/main.rs b/examples/sftp/src/main.rs index 2f2bf52..3c7c77f 100644 --- a/examples/sftp/src/main.rs +++ b/examples/sftp/src/main.rs @@ -15,13 +15,12 @@ use cloud_filter::{ placeholder::{ConvertOptions, Placeholder}, placeholder_file::PlaceholderFile, request::Request, - root::{HydrationType, PopulationType, Registration, SecurityId, Session, SyncRootIdBuilder}, + root::{HydrationType, PopulationType, SecurityId, Session, SyncRootIdBuilder, SyncRootInfo}, utility::{FileTime, WriteAt}, }; use rkyv::{Archive, Deserialize, Serialize}; use ssh2::Sftp; use thiserror::Error; -use widestring::{u16str, U16String}; // max should be 65536, this is done both in term-scp and sshfs because it's the // max packet size for a tcp connection @@ -51,27 +50,30 @@ fn main() { let sftp = session.sftp().unwrap(); - let sync_root_id = SyncRootIdBuilder::new(U16String::from_str(PROVIDER_NAME)) + let sync_root_id = SyncRootIdBuilder::new(PROVIDER_NAME) .user_security_id(SecurityId::current_user().unwrap()) .build(); let client_path = get_client_path(); if !sync_root_id.is_registered().unwrap() { - let u16_display_name = U16String::from_str(DISPLAY_NAME); - Registration::from_sync_root_id(&sync_root_id) - .display_name(&u16_display_name) - .hydration_type(HydrationType::Full) - .population_type(PopulationType::Full) - .icon( - U16String::from_str("%SystemRoot%\\system32\\charmap.exe"), - 0, + sync_root_id + .register( + SyncRootInfo::default() + .with_display_name(DISPLAY_NAME) + .with_hydration_type(HydrationType::Full) + .with_population_type(PopulationType::Full) + .with_icon("%SystemRoot%\\system32\\charmap.exe,0") + .with_version("1.0.0") + .with_recycle_bin_uri("http://cloudmirror.example.com/recyclebin") + .unwrap() + .with_path(Path::new(&client_path)) + .unwrap(), ) - .version(u16str!("1.0.0")) - .recycle_bin_uri(u16str!("http://cloudmirror.example.com/recyclebin")) - .register(Path::new(&client_path)) - .unwrap(); + .unwrap() } + println!("info: {:#?}", sync_root_id.info()); + mark_in_sync(Path::new(&client_path), &sftp); let connection = Session::new() diff --git a/src/ext/file.rs b/src/ext/file.rs index ae48a50..d4b5773 100644 --- a/src/ext/file.rs +++ b/src/ext/file.rs @@ -11,16 +11,12 @@ use windows::{ Win32::{ Foundation::HANDLE, Storage::CloudFilters::{ - self, CfDehydratePlaceholder, CfGetSyncRootInfoByHandle, CF_SYNC_PROVIDER_STATUS, - CF_SYNC_ROOT_STANDARD_INFO, + self, CfDehydratePlaceholder, CF_SYNC_PROVIDER_STATUS, CF_SYNC_ROOT_STANDARD_INFO, }, }, }; -use crate::{ - root::{HydrationPolicy, HydrationType, PopulationType, SupportedAttributes}, - sealed::Sealed, -}; +use crate::sealed::Sealed; /// An API extension to [File][std::fs::File]. pub trait FileExt: AsRawHandle + Sealed { @@ -35,37 +31,6 @@ pub trait FileExt: AsRawHandle + Sealed { dehydrate(self.as_raw_handle(), range, true) } - /// Gets various characteristics of the sync root. - fn sync_root_info(&self) -> core::Result { - // TODO: this except finds the size after 2 calls of CfGetSyncRootInfoByHandle - todo!() - } - - #[allow(clippy::missing_safety_doc)] - /// Gets various characteristics of a placeholder using the passed blob size. - unsafe fn sync_root_info_unchecked(&self, blob_size: usize) -> core::Result { - let mut data = vec![0; mem::size_of::() + blob_size]; - - unsafe { - CfGetSyncRootInfoByHandle( - HANDLE(self.as_raw_handle() as isize), - CloudFilters::CF_SYNC_ROOT_INFO_STANDARD, - data.as_mut_ptr() as *mut _, - data.len() as u32, - None, - )?; - } - - Ok(SyncRootInfo { - info: &unsafe { - data[..=mem::size_of::()] - .align_to::() - } - .1[0] as *const _, - data, - }) - } - /// Returns whether or not the handle is inside of a sync root. fn in_sync_root() -> core::Result { // TODO: this should use the uwp apis @@ -122,25 +87,25 @@ impl SyncRootInfo { unsafe { &*self.info }.SyncRootFileId as u64 } - /// The hydration policy of the sync root. - pub fn hydration_policy(&self) -> HydrationType { - unsafe { &*self.info }.HydrationPolicy.Primary.into() - } + // /// The hydration policy of the sync root. + // pub fn hydration_policy(&self) -> HydrationType { + // unsafe { &*self.info }.HydrationPolicy.Primary.into() + // } /// The hydration type of the sync root. - pub fn hydration_type(&self) -> HydrationPolicy { - unsafe { &*self.info }.HydrationPolicy.Modifier.into() - } - - /// The population type of the sync root. - pub fn population_type(&self) -> PopulationType { - unsafe { &*self.info }.PopulationPolicy.Primary.into() - } - - /// The attributes supported by the sync root. - pub fn supported_attributes(&self) -> SupportedAttributes { - unsafe { &*self.info }.InSyncPolicy.into() - } + // pub fn hydration_type(&self) -> HydrationPolicy { + // unsafe { &*self.info }.HydrationPolicy.Modifier.into() + // } + + // /// The population type of the sync root. + // pub fn population_type(&self) -> PopulationType { + // unsafe { &*self.info }.PopulationPolicy.Primary.into() + // } + + // /// The attributes supported by the sync root. + // pub fn supported_attributes(&self) -> SupportedAttributes { + // unsafe { &*self.info }.InSyncPolicy.into() + // } /// Whether or not hardlinks are allowed by the sync root. pub fn hardlinks_allowed(&self) -> bool { diff --git a/src/root/mod.rs b/src/root/mod.rs index 4730527..31dc2a5 100644 --- a/src/root/mod.rs +++ b/src/root/mod.rs @@ -1,12 +1,12 @@ mod connect; -mod register; mod session; mod sync_root_id; +mod sync_root_info; pub use connect::Connection; -pub use register::{ - HydrationPolicy, HydrationType, PopulationType, ProtectionMode, Registration, - SupportedAttributes, -}; pub use session::Session; pub use sync_root_id::{active_roots, is_supported, SecurityId, SyncRootId, SyncRootIdBuilder}; +pub use sync_root_info::{ + HydrationPolicy, HydrationType, PopulationType, ProtectionMode, SupportedAttribute, + SyncRootInfo, +}; diff --git a/src/root/register.rs b/src/root/register.rs deleted file mode 100644 index 2c00e8b..0000000 --- a/src/root/register.rs +++ /dev/null @@ -1,375 +0,0 @@ -use std::path::Path; - -use widestring::{U16Str, U16String}; -use windows::{ - core::{self, GUID}, - Foundation::Uri, - Storage::{ - Provider::{ - StorageProviderHardlinkPolicy, StorageProviderHydrationPolicy, - StorageProviderHydrationPolicyModifier, StorageProviderInSyncPolicy, - StorageProviderPopulationPolicy, StorageProviderProtectionMode, - StorageProviderSyncRootInfo, StorageProviderSyncRootManager, - }, - StorageFolder, - Streams::DataWriter, - }, - Win32::Storage::CloudFilters::{ - self, CF_HYDRATION_POLICY_MODIFIER, CF_HYDRATION_POLICY_PRIMARY, CF_INSYNC_POLICY, - CF_POPULATION_POLICY_PRIMARY, - }, -}; - -use crate::utility::ToHString; - -use super::SyncRootId; - -#[derive(Debug, Clone)] -pub struct Registration<'a> { - sync_root_id: &'a SyncRootId, - show_siblings_as_group: bool, - allow_pinning: bool, - allow_hardlinks: bool, - display_name: &'a U16Str, - recycle_bin_uri: Option<&'a U16Str>, - version: Option<&'a U16Str>, - hydration_type: HydrationType, - hydration_policy: HydrationPolicy, - population_type: PopulationType, - protection_mode: ProtectionMode, - provider_id: Option, - supported_attributes: SupportedAttributes, - icon: U16String, - blob: Option<&'a [u8]>, -} - -impl<'a> Registration<'a> { - pub fn from_sync_root_id(sync_root_id: &'a SyncRootId) -> Self { - Self { - sync_root_id, - display_name: sync_root_id.as_u16_str(), - recycle_bin_uri: None, - show_siblings_as_group: false, - allow_pinning: false, - version: None, - provider_id: None, - protection_mode: ProtectionMode::Unknown, - allow_hardlinks: false, - hydration_type: HydrationType::Progressive, // stated as default in the docs - hydration_policy: HydrationPolicy::default(), - population_type: PopulationType::Full, - supported_attributes: SupportedAttributes::default(), - icon: U16String::from_str("C:\\Windows\\System32\\imageres.dll,1525"), - blob: None, - } - } - - pub fn hydration_type(mut self, hydration_type: HydrationType) -> Self { - self.hydration_type = hydration_type; - self - } - - pub fn allow_pinning(mut self) -> Self { - self.allow_pinning = true; - self - } - - pub fn allow_hardlinks(mut self) -> Self { - self.allow_hardlinks = true; - self - } - - // This field is required - - pub fn display_name(mut self, display_name: &'a U16Str) -> Self { - self.display_name = display_name; - self - } - - pub fn recycle_bin_uri(mut self, uri: &'a U16Str) -> Self { - self.recycle_bin_uri = Some(uri); - self - } - - // I think this is for sync roots with the same provider name? - - pub fn show_siblings_as_group(mut self) -> Self { - self.show_siblings_as_group = true; - self - } - - pub fn population_type(mut self, population_type: PopulationType) -> Self { - self.population_type = population_type; - self - } - - pub fn version(mut self, version: &'a U16Str) -> Self { - assert!( - version.len() <= CloudFilters::CF_MAX_PROVIDER_VERSION_LENGTH as usize, - "version length must not exceed {} characters, got {} characters", - CloudFilters::CF_MAX_PROVIDER_VERSION_LENGTH, - version.len() - ); - self.version = Some(version); - self - } - - pub fn protection_mode(mut self, protection_mode: ProtectionMode) -> Self { - self.protection_mode = protection_mode; - self - } - - pub fn supported_attributes(mut self, supported_attributes: SupportedAttributes) -> Self { - self.supported_attributes = supported_attributes; - self - } - - pub fn hydration_policy(mut self, hydration_policy: HydrationPolicy) -> Self { - self.hydration_policy = hydration_policy; - self - } - - // TODO: this field is required - // https://docs.microsoft.com/en-us/windows/win32/menurc/icon-resource - - pub fn icon(mut self, mut path: U16String, index: u16) -> Self { - path.push_str(format!(",{index}")); - self.icon = path; - self - } - - pub fn blob(mut self, blob: &'a [u8]) -> Self { - assert!( - blob.len() <= 65536, - "blob size must not exceed 65536 bytes, got {} bytes", - blob.len() - ); - self.blob = Some(blob); - self - } - - pub fn register>(&self, path: P) -> core::Result<()> { - let info = StorageProviderSyncRootInfo::new()?; - - info.SetProtectionMode(self.protection_mode.into())?; - info.SetShowSiblingsAsGroup(self.show_siblings_as_group)?; - info.SetHydrationPolicy(self.hydration_type.into())?; - info.SetHydrationPolicyModifier(self.hydration_policy.0)?; - info.SetPopulationPolicy(self.population_type.into())?; - info.SetInSyncPolicy(self.supported_attributes.0)?; - info.SetDisplayNameResource(&self.display_name.to_hstring())?; - info.SetIconResource(&self.icon.to_hstring())?; - info.SetPath( - &StorageFolder::GetFolderFromPathAsync( - &U16String::from_os_str(path.as_ref().as_os_str()).to_hstring(), - )? - .get()?, - )?; - info.SetHardlinkPolicy(if self.allow_hardlinks { - StorageProviderHardlinkPolicy::Allowed - } else { - StorageProviderHardlinkPolicy::None - })?; - info.SetId(self.sync_root_id.as_hstring())?; - - if let Some(provider_id) = self.provider_id { - info.SetProviderId(provider_id)?; - } - if let Some(version) = &self.version { - info.SetVersion(&version.to_hstring())?; - } - - if let Some(uri) = &self.recycle_bin_uri { - info.SetRecycleBinUri(&Uri::CreateUri(&uri.to_hstring())?)?; - } - if let Some(blob) = &self.blob { - // TODO: implement IBuffer interface for slices to avoid a copy - let writer = DataWriter::new()?; - writer.WriteBytes(blob)?; - info.SetContext(&writer.DetachBuffer()?)?; - } - - StorageProviderSyncRootManager::Register(&info) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ProtectionMode { - Personal, - Unknown, -} - -impl From for StorageProviderProtectionMode { - fn from(mode: ProtectionMode) -> Self { - match mode { - ProtectionMode::Personal => StorageProviderProtectionMode::Personal, - ProtectionMode::Unknown => StorageProviderProtectionMode::Unknown, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum HydrationType { - Partial, - Progressive, - Full, - AlwaysFull, -} - -impl From for StorageProviderHydrationPolicy { - fn from(hydration_type: HydrationType) -> Self { - match hydration_type { - HydrationType::Partial => StorageProviderHydrationPolicy::Partial, - HydrationType::Progressive => StorageProviderHydrationPolicy::Progressive, - HydrationType::Full => StorageProviderHydrationPolicy::Full, - HydrationType::AlwaysFull => StorageProviderHydrationPolicy::AlwaysFull, - } - } -} - -impl From for HydrationType { - fn from(primary: CF_HYDRATION_POLICY_PRIMARY) -> Self { - match primary { - CloudFilters::CF_HYDRATION_POLICY_PARTIAL => HydrationType::Partial, - CloudFilters::CF_HYDRATION_POLICY_PROGRESSIVE => HydrationType::Progressive, - CloudFilters::CF_HYDRATION_POLICY_FULL => HydrationType::Full, - CloudFilters::CF_HYDRATION_POLICY_ALWAYS_FULL => HydrationType::AlwaysFull, - _ => unreachable!(), - } - } -} - -#[derive(Debug, Clone, Copy)] -pub struct HydrationPolicy(pub(crate) StorageProviderHydrationPolicyModifier); - -impl HydrationPolicy { - pub fn new() -> Self { - Self::default() - } - - pub fn require_validation(mut self) -> Self { - self.0 |= StorageProviderHydrationPolicyModifier::ValidationRequired; - self - } - - // TODO: assert this, it is incompatible with the validation required parameter - // https://docs.microsoft.com/en-us/windows/win32/api/cfapi/ne-cfapi-cf_hydration_policy_modifier - - pub fn allow_streaming(mut self) -> Self { - self.0 |= StorageProviderHydrationPolicyModifier::StreamingAllowed; - self - } - - pub fn allow_platform_dehydration(mut self) -> Self { - self.0 |= StorageProviderHydrationPolicyModifier::AutoDehydrationAllowed; - self - } - - pub fn allow_full_restart_hydration(mut self) -> Self { - self.0 |= StorageProviderHydrationPolicyModifier::AllowFullRestartHydration; - self - } -} - -impl Default for HydrationPolicy { - fn default() -> Self { - Self(StorageProviderHydrationPolicyModifier::None) - } -} - -impl From for HydrationPolicy { - fn from(primary: CF_HYDRATION_POLICY_MODIFIER) -> Self { - Self(StorageProviderHydrationPolicyModifier(primary.0 as u32)) - } -} - -#[derive(Debug, Clone, Copy)] -pub enum PopulationType { - Full, - AlwaysFull, -} - -impl From for StorageProviderPopulationPolicy { - fn from(population_type: PopulationType) -> StorageProviderPopulationPolicy { - match population_type { - PopulationType::Full => StorageProviderPopulationPolicy::Full, - PopulationType::AlwaysFull => StorageProviderPopulationPolicy::AlwaysFull, - } - } -} - -impl From for PopulationType { - fn from(primary: CF_POPULATION_POLICY_PRIMARY) -> Self { - match primary { - CloudFilters::CF_POPULATION_POLICY_FULL => PopulationType::Full, - CloudFilters::CF_POPULATION_POLICY_ALWAYS_FULL => PopulationType::AlwaysFull, - _ => unreachable!(), - } - } -} - -#[derive(Debug, Clone, Copy)] -pub struct SupportedAttributes(pub(crate) StorageProviderInSyncPolicy); - -impl SupportedAttributes { - pub fn new() -> Self { - Self::default() - } - - pub fn file_creation_time(mut self) -> Self { - self.0 |= StorageProviderInSyncPolicy::FileCreationTime; - self - } - - pub fn file_readonly(mut self) -> Self { - self.0 |= StorageProviderInSyncPolicy::FileReadOnlyAttribute; - self - } - - pub fn file_hidden(mut self) -> Self { - self.0 |= StorageProviderInSyncPolicy::FileHiddenAttribute; - self - } - - pub fn file_system(mut self) -> Self { - self.0 |= StorageProviderInSyncPolicy::FileSystemAttribute; - self - } - - pub fn file_last_write_time(mut self) -> Self { - self.0 |= StorageProviderInSyncPolicy::FileLastWriteTime; - self - } - - pub fn directory_creation_time(mut self) -> Self { - self.0 |= StorageProviderInSyncPolicy::DirectoryCreationTime; - self - } - - pub fn directory_readonly(mut self) -> Self { - self.0 |= StorageProviderInSyncPolicy::DirectoryReadOnlyAttribute; - self - } - - pub fn directory_hidden(mut self) -> Self { - self.0 |= StorageProviderInSyncPolicy::DirectoryHiddenAttribute; - self - } - - pub fn directory_last_write_time(mut self) -> Self { - self.0 |= StorageProviderInSyncPolicy::DirectoryLastWriteTime; - self - } -} - -impl Default for SupportedAttributes { - fn default() -> Self { - Self(StorageProviderInSyncPolicy::Default) - } -} - -impl From for SupportedAttributes { - fn from(policy: CF_INSYNC_POLICY) -> Self { - Self(StorageProviderInSyncPolicy(policy.0)) - } -} diff --git a/src/root/sync_root_id.rs b/src/root/sync_root_id.rs index 86a4491..eb798f3 100644 --- a/src/root/sync_root_id.rs +++ b/src/root/sync_root_id.rs @@ -1,7 +1,7 @@ use std::{ - ffi::OsString, + ffi::{OsStr, OsString}, mem::MaybeUninit, - os::windows::ffi::{OsStrExt, OsStringExt}, + os::windows::ffi::OsStringExt, path::Path, ptr, }; @@ -9,7 +9,7 @@ use std::{ use widestring::{U16CStr, U16Str, U16String}; use windows::{ core::{self, HSTRING, PWSTR}, - Storage::Provider::{StorageProviderSyncRootInfo, StorageProviderSyncRootManager}, + Storage::Provider::StorageProviderSyncRootManager, Win32::{ Foundation::{self, LocalFree, ERROR_INSUFFICIENT_BUFFER, HANDLE, HLOCAL}, Security::{self, Authorization::ConvertSidToStringSidW, GetTokenInformation, TOKEN_USER}, @@ -19,9 +19,12 @@ use windows::{ use crate::{ext::PathExt, utility::ToHString}; +use super::SyncRootInfo; + /// Returns a list of active sync roots. -pub fn active_roots() -> core::Result> { - StorageProviderSyncRootManager::GetCurrentSyncRoots().map(|list| list.into_iter().collect()) +pub fn active_roots() -> core::Result> { + StorageProviderSyncRootManager::GetCurrentSyncRoots() + .map(|list| list.into_iter().map(SyncRootInfo).collect()) } /// Returns whether or not the Cloud Filter API is supported (or at least the UWP part of it, for @@ -47,23 +50,22 @@ impl SyncRootIdBuilder { /// # Panics /// /// Panics if the provider name is longer than 255 characters or contains exclamation points. - pub fn new(provider_name: U16String) -> Self { + pub fn new(provider_name: impl AsRef) -> Self { + let name = U16String::from_os_str(&provider_name); + assert!( - provider_name.len() <= CloudFilters::CF_MAX_PROVIDER_NAME_LENGTH as usize, + name.len() <= CloudFilters::CF_MAX_PROVIDER_NAME_LENGTH as usize, "provider name must not exceed {} characters, got {} characters", CloudFilters::CF_MAX_PROVIDER_NAME_LENGTH, - provider_name.len() + name.len() ); assert!( - !provider_name - .as_slice() - .iter() - .any(|c| *c == SyncRootId::SEPARATOR), + !name.as_slice().iter().any(|c| *c == SyncRootId::SEPARATOR), "provider name must not contain exclamation points" ); Self { - provider_name, + provider_name: name, user_security_id: SecurityId(U16String::new()), account_name: U16String::new(), } @@ -82,8 +84,8 @@ impl SyncRootIdBuilder { /// /// This value does not have any actual meaning and it could theoretically be anything. /// However, it is encouraged to set this value to the account name of the user on the remote. - pub fn account_name(mut self, account_name: U16String) -> Self { - self.account_name = account_name; + pub fn account_name(mut self, account_name: impl AsRef) -> Self { + self.account_name = U16String::from_os_str(&account_name); self } @@ -110,7 +112,7 @@ impl SyncRootIdBuilder { /// /// A [SyncRootId] stores an inner, reference counted [HSTRING][windows::core::HSTRING], making this struct cheap to clone. #[derive(Debug, Clone)] -pub struct SyncRootId(HSTRING); +pub struct SyncRootId(pub(crate) HSTRING); impl SyncRootId { // https://docs.microsoft.com/en-us/uwp/api/windows.storage.provider.storageprovidersyncrootinfo.id?view=winrt-22000#windows-storage-provider-storageprovidersyncrootinfo-id @@ -134,8 +136,14 @@ impl SyncRootId { } /// Returns the sync root information for the [SyncRootId]. - pub fn sync_root_info(&self) -> core::Result { - StorageProviderSyncRootManager::GetSyncRootInformationForId(&self.0) + pub fn info(&self) -> core::Result { + StorageProviderSyncRootManager::GetSyncRootInformationForId(&self.0).map(SyncRootInfo) + } + + /// Registers the sync root at the current [SyncRootId]. + pub fn register(&self, info: SyncRootInfo) -> core::Result<()> { + info.0.SetId(&self.0).unwrap(); + StorageProviderSyncRootManager::Register(&info.0) } /// Unregisters the sync root at the current [SyncRootId] if it exists. @@ -196,15 +204,14 @@ impl SecurityId { /// # Panics /// /// Panics if the security id contains an exclamation point. - pub fn new(id: OsString) -> Self { + pub fn new(id: impl AsRef) -> Self { + let id = U16String::from_os_str(&id); assert!( - !id.as_os_str() - .encode_wide() - .any(|x| x == SyncRootId::SEPARATOR), + !id.as_slice().iter().any(|x| *x == SyncRootId::SEPARATOR), "security id cannot contain exclamation points" ); - Self(id.into()) + Self(id) } /// The [SecurityId] for the logged in user. diff --git a/src/root/sync_root_info.rs b/src/root/sync_root_info.rs new file mode 100644 index 0000000..9d80ea6 --- /dev/null +++ b/src/root/sync_root_info.rs @@ -0,0 +1,473 @@ +use std::{ + ffi::{OsStr, OsString}, + os::windows::ffi::OsStringExt, + path::{Path, PathBuf}, +}; + +use flagset::{flags, FlagSet}; +use widestring::U16String; +use windows::{ + core::Result, + Foundation::Uri, + Storage::{ + Provider::{ + StorageProviderHardlinkPolicy, StorageProviderHydrationPolicy, + StorageProviderHydrationPolicyModifier, StorageProviderInSyncPolicy, + StorageProviderPopulationPolicy, StorageProviderProtectionMode, + StorageProviderSyncRootInfo, + }, + StorageFolder, + Streams::{DataReader, DataWriter}, + }, +}; + +use crate::utility::ToHString; + +use super::SyncRootId; + +#[derive(Clone)] +pub struct SyncRootInfo(pub(crate) StorageProviderSyncRootInfo); + +impl SyncRootInfo { + /// Enables or disables the ability for files to be made available offline. + pub fn allow_pinning(&self) -> bool { + self.0.AllowPinning().unwrap() + } + + /// Sets the ability for files to be made available offline. + pub fn set_allow_pinning(&mut self, allow_pinning: bool) { + self.0.SetAllowPinning(allow_pinning).unwrap() + } + + /// Sets the ability for files to be made available offline. + pub fn with_allow_pinning(mut self, allow_pinning: bool) -> Self { + self.set_allow_pinning(allow_pinning); + self + } + + /// Hard links are allowed on a placeholder within the same sync root. + pub fn allow_hardlinks(&self) -> bool { + self.0.HardlinkPolicy().unwrap() == StorageProviderHardlinkPolicy::Allowed + } + + /// Sets the hard link are allowed on a placeholder within the same sync root. + pub fn set_allow_hardlinks(&mut self, allow_hardlinks: bool) { + self.0 + .SetHardlinkPolicy(if allow_hardlinks { + StorageProviderHardlinkPolicy::Allowed + } else { + StorageProviderHardlinkPolicy::None + }) + .unwrap() + } + + /// Sets the hard link are allowed on a placeholder within the same sync root. + pub fn with_allow_hardlinks(mut self, allow_hardlinks: bool) -> Self { + self.set_allow_hardlinks(allow_hardlinks); + self + } + + /// An optional display name that maps to the existing sync root registration. + pub fn display_name(&self) -> OsString { + self.0.DisplayNameResource().unwrap().to_os_string() + } + + /// Sets the display name that maps to the existing sync root registration. + pub fn set_display_name(&mut self, display_name: impl AsRef) { + self.0 + .SetDisplayNameResource(&U16String::from_os_str(&display_name).to_hstring()) + .unwrap() + } + + /// Sets the display name that maps to the existing sync root registration. + pub fn with_display_name(mut self, display_name: impl AsRef) -> Self { + self.set_display_name(display_name); + self + } + + /// A Uri to a cloud storage recycle bin. + pub fn recycle_bin_uri(&self) -> Option { + self.0 + .RecycleBinUri() + .map(|uri| uri.ToString().unwrap().to_os_string()) + .ok() + } + + /// Sets the Uri to a cloud storage recycle bin. + /// + /// Returns an error if the Uri is not valid. + pub fn set_recycle_bin_uri(&mut self, recycle_bin_uri: impl AsRef) -> Result<()> { + self.0 + .SetRecycleBinUri(&Uri::CreateUri( + &U16String::from_os_str(&recycle_bin_uri).to_hstring(), + )?) + .unwrap(); + + Ok(()) + } + + /// Sets the Uri to a cloud storage recycle bin. + /// + /// Returns an error if the Uri is not valid. + pub fn with_recycle_bin_uri(mut self, recycle_bin_uri: impl AsRef) -> Result { + self.set_recycle_bin_uri(recycle_bin_uri)?; + Ok(self) + } + + /// Shows sibling sync roots listed under the main sync root in the File Explorer. + pub fn show_siblings_as_group(&self) -> bool { + self.0.ShowSiblingsAsGroup().unwrap() + } + + /// Shows sibling sync roots listed under the main sync root in the File Explorer or not. + pub fn set_show_siblings_as_group(&mut self, show_siblings_as_group: bool) { + self.0 + .SetShowSiblingsAsGroup(show_siblings_as_group) + .unwrap() + } + + /// Shows sibling sync roots listed under the main sync root in the File Explorer or not. + pub fn with_show_siblings_as_group(mut self, show_siblings_as_group: bool) -> Self { + self.set_show_siblings_as_group(show_siblings_as_group); + self + } + + /// The path of the sync root. + pub fn path(&self) -> PathBuf { + self.0 + .Path() + .map(|path| path.Path().unwrap().to_os_string().into()) + .unwrap_or_default() + } + + /// Sets the path of the sync root. + /// + /// Returns an error if the path is not a folder. + pub fn set_path(&mut self, path: impl AsRef) -> Result<()> { + self.0 + .SetPath( + &StorageFolder::GetFolderFromPathAsync( + &U16String::from_os_str(path.as_ref()).to_hstring(), + ) + .unwrap() + .get()?, + ) + .unwrap(); + Ok(()) + } + + /// Sets the path of the sync root. + /// + /// Returns an error if the path is not a folder. + pub fn with_path(mut self, path: impl AsRef) -> Result { + self.set_path(path)?; + Ok(self) + } + + /// The population policy of the sync root registration. + pub fn population_type(&self) -> PopulationType { + self.0.PopulationPolicy().unwrap().into() + } + + /// Sets the population policy of the sync root registration. + pub fn set_population_type(&mut self, population_type: PopulationType) { + self.0.SetPopulationPolicy(population_type.into()).unwrap(); + } + + /// Sets the population policy of the sync root registration. + pub fn with_population_type(mut self, population_type: PopulationType) -> Self { + self.set_population_type(population_type); + self + } + + /// The version number of the sync root provider. + pub fn version(&self) -> OsString { + OsString::from_wide(self.0.Version().unwrap().as_wide()) + } + + /// Sets the version number of the sync root provider. + pub fn set_version(&mut self, version: impl AsRef) { + self.0 + .SetVersion(&U16String::from_os_str(&version).to_hstring()) + .unwrap() + } + + /// Sets the version number of the sync root provider. + pub fn with_version(mut self, version: impl AsRef) -> Self { + self.set_version(version); + self + } + + /// The protection mode of the sync root registration. + pub fn protection_mode(&self) -> ProtectionMode { + self.0.ProtectionMode().unwrap().into() + } + + /// Sets the protection mode of the sync root registration. + pub fn set_protection_mode(&mut self, protection_mode: ProtectionMode) { + self.0.SetProtectionMode(protection_mode.into()).unwrap(); + } + + /// Sets the protection mode of the sync root registration. + pub fn with_protection_mode(mut self, protection_mode: ProtectionMode) -> Self { + self.set_protection_mode(protection_mode); + self + } + + /// The supported attributes of the sync root registration. + pub fn supported_attribute(&self) -> FlagSet { + FlagSet::new(self.0.InSyncPolicy().unwrap().0).expect("flags should be valid") + } + + /// Sets the supported attributes of the sync root registration. + pub fn set_supported_attribute( + &mut self, + supported_attribute: impl Into>, + ) { + self.0 + .SetInSyncPolicy(StorageProviderInSyncPolicy( + supported_attribute.into().bits(), + )) + .unwrap(); + } + + /// Sets the supported attributes of the sync root registration. + pub fn with_supported_attribute( + mut self, + supported_attribute: impl Into>, + ) -> Self { + self.set_supported_attribute(supported_attribute); + self + } + + /// The hydration policy of the sync root registration. + pub fn hydration_type(&self) -> HydrationType { + self.0.HydrationPolicy().unwrap().into() + } + + /// Sets the hydration policy of the sync root registration. + pub fn set_hydration_type(&mut self, hydration_type: HydrationType) { + self.0.SetHydrationPolicy(hydration_type.into()).unwrap(); + } + + /// Sets the hydration policy of the sync root registration. + pub fn with_hydration_type(mut self, hydration_type: HydrationType) -> Self { + self.set_hydration_type(hydration_type); + self + } + + /// The hydration policy of the sync root registration. + pub fn hydration_policy(&self) -> FlagSet { + FlagSet::new(self.0.HydrationPolicyModifier().unwrap().0).expect("flags should be valid") + } + + /// Sets the hydration policy of the sync root registration. + pub fn set_hydration_policy(&mut self, hydration_policy: impl Into>) { + self.0 + .SetHydrationPolicyModifier(StorageProviderHydrationPolicyModifier( + hydration_policy.into().bits(), + )) + .unwrap(); + } + + /// Sets the hydration policy of the sync root registration. + pub fn with_hydration_policy( + mut self, + hydration_policy: impl Into>, + ) -> Self { + self.set_hydration_policy(hydration_policy); + self + } + + /// The icon of the sync root registration. + pub fn icon(&self) -> OsString { + self.0.IconResource().unwrap().to_os_string() + } + + /// Sets the icon of the sync root registration. + /// + /// See also . + pub fn set_icon(&mut self, icon: impl AsRef) { + self.0 + .SetIconResource(&U16String::from_os_str(&icon).to_hstring()) + .unwrap(); + } + + /// Sets the icon of the sync root registration. + /// + /// See also . + pub fn with_icon(mut self, icon: impl AsRef) -> Self { + self.set_icon(icon); + self + } + + /// The identifier of the sync root registration. + pub fn id(&self) -> SyncRootId { + SyncRootId(self.0.Id().unwrap()) + } + + /// The blob of the sync root registration. + pub fn blob(&self) -> Vec { + let Ok(buffer) = self.0.Context() else { + return Vec::new(); + }; + let mut data = vec![0u8; buffer.Length().unwrap() as usize]; + let reader = DataReader::FromBuffer(&buffer).unwrap(); + reader.ReadBytes(data.as_mut_slice()).unwrap(); + + data + } + + /// Sets the blob of the sync root registration. + pub fn set_blob(&mut self, blob: &[u8]) { + let writer = DataWriter::new().unwrap(); + writer.WriteBytes(blob).unwrap(); + self.0.SetContext(&writer.DetachBuffer().unwrap()).unwrap(); + } + + /// Sets the blob of the sync root registration. + pub fn with_blob(mut self, blob: &[u8]) -> Self { + self.set_blob(blob); + self + } +} + +impl Default for SyncRootInfo { + fn default() -> Self { + Self(StorageProviderSyncRootInfo::new().unwrap()) + } +} + +/// The protection mode of the sync root registration. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProtectionMode { + /// The sync root should only contain personal files, not encrypted or business related files. + Personal, + /// The sync root can contain any type of file. + Unknown, +} + +impl From for StorageProviderProtectionMode { + fn from(mode: ProtectionMode) -> Self { + match mode { + ProtectionMode::Personal => StorageProviderProtectionMode::Personal, + ProtectionMode::Unknown => StorageProviderProtectionMode::Unknown, + } + } +} + +impl From for ProtectionMode { + fn from(mode: StorageProviderProtectionMode) -> Self { + match mode { + StorageProviderProtectionMode::Personal => ProtectionMode::Personal, + StorageProviderProtectionMode::Unknown => ProtectionMode::Unknown, + _ => unreachable!(), + } + } +} + +flags! { + /// Attributes supported by the sync root. + pub enum SupportedAttribute: u32 { + FileCreationTime = StorageProviderInSyncPolicy::FileCreationTime.0, + FileReadonly = StorageProviderInSyncPolicy::FileReadOnlyAttribute.0, + FileHidden = StorageProviderInSyncPolicy::FileHiddenAttribute.0, + FileSystem = StorageProviderInSyncPolicy::FileSystemAttribute.0, + FileLastWriteTime = StorageProviderInSyncPolicy::FileLastWriteTime.0, + DirectoryCreationTime = StorageProviderInSyncPolicy::DirectoryCreationTime.0, + DirectoryReadonly = StorageProviderInSyncPolicy::DirectoryReadOnlyAttribute.0, + DirectoryHidden = StorageProviderInSyncPolicy::DirectoryHiddenAttribute.0, + DirectoryLastWriteTime = StorageProviderInSyncPolicy::DirectoryLastWriteTime.0, + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HydrationType { + Partial, + Progressive, + Full, + AlwaysFull, +} + +impl From for StorageProviderHydrationPolicy { + fn from(hydration_type: HydrationType) -> Self { + match hydration_type { + HydrationType::Partial => StorageProviderHydrationPolicy::Partial, + HydrationType::Progressive => StorageProviderHydrationPolicy::Progressive, + HydrationType::Full => StorageProviderHydrationPolicy::Full, + HydrationType::AlwaysFull => StorageProviderHydrationPolicy::AlwaysFull, + } + } +} + +impl From for HydrationType { + fn from(policy: StorageProviderHydrationPolicy) -> Self { + match policy { + StorageProviderHydrationPolicy::Partial => HydrationType::Partial, + StorageProviderHydrationPolicy::Progressive => HydrationType::Progressive, + StorageProviderHydrationPolicy::Full => HydrationType::Full, + StorageProviderHydrationPolicy::AlwaysFull => HydrationType::AlwaysFull, + _ => unreachable!(), + } + } +} + +flags! { + /// Hydration policy + pub enum HydrationPolicy: u32 { + ValidationRequired = StorageProviderHydrationPolicyModifier::ValidationRequired.0, + StreamingAllowed = StorageProviderHydrationPolicyModifier::StreamingAllowed.0, + AutoDehydrationAllowed = StorageProviderHydrationPolicyModifier::AutoDehydrationAllowed.0, + AllowFullRestartHydration = StorageProviderHydrationPolicyModifier::AllowFullRestartHydration.0, + } +} + +/// The population policy of the sync root registration. +#[derive(Debug, Clone, Copy)] +pub enum PopulationType { + /// If the placeholder files or directories are not fully populated, + /// the platform will request that the sync provider populate them before completing a user request. + Full, + /// The platform will assume that placeholder files and directories are always available locally. + AlwaysFull, +} + +impl From for StorageProviderPopulationPolicy { + fn from(population_type: PopulationType) -> StorageProviderPopulationPolicy { + match population_type { + PopulationType::Full => StorageProviderPopulationPolicy::Full, + PopulationType::AlwaysFull => StorageProviderPopulationPolicy::AlwaysFull, + } + } +} + +impl From for PopulationType { + fn from(population_type: StorageProviderPopulationPolicy) -> Self { + match population_type { + StorageProviderPopulationPolicy::Full => PopulationType::Full, + StorageProviderPopulationPolicy::AlwaysFull => PopulationType::AlwaysFull, + _ => unreachable!(), + } + } +} + +impl std::fmt::Debug for SyncRootInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SyncRootInfo") + .field("allow_pinning", &self.allow_pinning()) + .field("allow_hardlinks", &self.allow_hardlinks()) + .field("display name", &self.display_name()) + .field("recycle_bin_uri", &self.recycle_bin_uri()) + .field("hydration_policy", &self.hydration_policy()) + .field("hydration_type", &self.hydration_type()) + .field("icon", &self.icon()) + .field("path", &self.path()) + .field("population_type", &self.population_type()) + .field("protection_mode", &self.protection_mode()) + .field("supported_attribute", &self.supported_attribute()) + .field("show_siblings_as_group", &self.show_siblings_as_group()) + .field("id", &self.id()) + .field("version", &self.version()) + .finish() + } +} From 67bf486f10b88ddd6d3ece0dae84b2ffe08a3503 Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Sun, 14 Jul 2024 16:42:45 +0800 Subject: [PATCH 22/34] chore: check fields of SyncRootInfo --- examples/sftp/src/main.rs | 2 -- src/root/sync_root_id.rs | 25 +++++++++++++++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/examples/sftp/src/main.rs b/examples/sftp/src/main.rs index 3c7c77f..eef7de8 100644 --- a/examples/sftp/src/main.rs +++ b/examples/sftp/src/main.rs @@ -72,8 +72,6 @@ fn main() { .unwrap() } - println!("info: {:#?}", sync_root_id.info()); - mark_in_sync(Path::new(&client_path), &sftp); let connection = Session::new() diff --git a/src/root/sync_root_id.rs b/src/root/sync_root_id.rs index eb798f3..a5d869d 100644 --- a/src/root/sync_root_id.rs +++ b/src/root/sync_root_id.rs @@ -8,10 +8,12 @@ use std::{ use widestring::{U16CStr, U16Str, U16String}; use windows::{ - core::{self, HSTRING, PWSTR}, + core::{self, Error, HSTRING, PWSTR}, Storage::Provider::StorageProviderSyncRootManager, Win32::{ - Foundation::{self, LocalFree, ERROR_INSUFFICIENT_BUFFER, HANDLE, HLOCAL}, + Foundation::{ + self, LocalFree, ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_PARAMETER, HANDLE, HLOCAL, + }, Security::{self, Authorization::ConvertSidToStringSidW, GetTokenInformation, TOKEN_USER}, Storage::CloudFilters, }, @@ -141,7 +143,26 @@ impl SyncRootId { } /// Registers the sync root at the current [SyncRootId]. + /// + /// [SyncRootInfo::display_name], [SyncRootInfo::icon], [SyncRootInfo::version] and [SyncRootInfo::path] + /// are required and cannot be empty. pub fn register(&self, info: SyncRootInfo) -> core::Result<()> { + macro_rules! check_field { + ($info:ident, $field:ident) => { + if $info.$field().eq(OsStr::new("")) { + Err(Error::new( + ERROR_INVALID_PARAMETER.to_hresult(), + U16String::from_str(&concat!(stringify!($field), " cannot be empty")) + .to_hstring(), + ))?; + } + }; + } + check_field!(info, display_name); + check_field!(info, icon); + check_field!(info, version); + check_field!(info, path); + info.0.SetId(&self.0).unwrap(); StorageProviderSyncRootManager::Register(&info.0) } From c0e31c06dcca19b41af025558addd447825fa74d Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Sun, 14 Jul 2024 17:51:14 +0800 Subject: [PATCH 23/34] refactor: remove path ext --- src/ext/mod.rs | 2 -- src/ext/path.rs | 48 ---------------------------------------- src/root/sync_root_id.rs | 13 ++++++++--- 3 files changed, 10 insertions(+), 53 deletions(-) delete mode 100644 src/ext/path.rs diff --git a/src/ext/mod.rs b/src/ext/mod.rs index a602d3c..b76225b 100644 --- a/src/ext/mod.rs +++ b/src/ext/mod.rs @@ -1,5 +1,3 @@ mod file; -mod path; pub use file::{FileExt, ProviderStatus, SyncRootInfo}; -pub use path::PathExt; diff --git a/src/ext/path.rs b/src/ext/path.rs deleted file mode 100644 index 31382c0..0000000 --- a/src/ext/path.rs +++ /dev/null @@ -1,48 +0,0 @@ -use std::path::Path; - -use widestring::U16String; -use windows::{ - core, - Storage::{ - Provider::{StorageProviderSyncRootInfo, StorageProviderSyncRootManager}, - StorageFolder, - }, -}; - -use crate::utility::ToHString; - -/// An API extension to [Path][std::path::Path] -pub trait PathExt -where - Self: AsRef, -{ - /// Whether or not the path is located inside of a sync root. - fn in_sync_root(&self) -> bool { - self.sync_root_info().is_ok() - } - - // TODO: This call requires a struct to be made for getters of StorageProviderSyncRootInfo - /// Information about the sync root that the path is located in. - fn sync_root_info(&self) -> core::Result { - StorageProviderSyncRootManager::GetSyncRootInformationForFolder( - &StorageFolder::GetFolderFromPathAsync( - &U16String::from_os_str(self.as_ref().as_os_str()).to_hstring(), - )? - .get()?, - ) - } - - // FIXME: This function is not work at all, the CF_PLACEHOLDER_STATE always be 0 or 1 - // fn placeholder_state(&self) -> core::Result { - // let path = U16CString::from_os_str(self.as_ref()).unwrap(); - // let mut file_data = MaybeUninit::zeroed(); - // unsafe { - // FindFirstFileW(PCWSTR(path.as_ptr()), file_data.as_mut_ptr()); - // Ok(CfGetPlaceholderStateFromFindData( - // file_data.assume_init_ref() as *const _ as *const _, - // )) - // } - // } -} - -impl> PathExt for T {} diff --git a/src/root/sync_root_id.rs b/src/root/sync_root_id.rs index a5d869d..046024d 100644 --- a/src/root/sync_root_id.rs +++ b/src/root/sync_root_id.rs @@ -9,7 +9,7 @@ use std::{ use widestring::{U16CStr, U16Str, U16String}; use windows::{ core::{self, Error, HSTRING, PWSTR}, - Storage::Provider::StorageProviderSyncRootManager, + Storage::{Provider::StorageProviderSyncRootManager, StorageFolder}, Win32::{ Foundation::{ self, LocalFree, ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_PARAMETER, HANDLE, HLOCAL, @@ -19,7 +19,7 @@ use windows::{ }, }; -use crate::{ext::PathExt, utility::ToHString}; +use crate::utility::ToHString; use super::SyncRootInfo; @@ -124,7 +124,14 @@ impl SyncRootId { /// Creates a [SyncRootId] from the sync root at the given path. pub fn from_path>(path: P) -> core::Result { // if the id is coming from a sync root, then it must be valid - Ok(Self(path.as_ref().sync_root_info()?.Id()?)) + StorageProviderSyncRootManager::GetSyncRootInformationForFolder( + &StorageFolder::GetFolderFromPathAsync( + &U16String::from_os_str(path.as_ref()).to_hstring(), + ) + .unwrap() + .get()?, + ) + .map(|info| SyncRootId(info.Id().unwrap())) } /// Whether or not the [SyncRootId] has already been registered. From ca6cf551d1be891c761efdf552d317e00e394513 Mon Sep 17 00:00:00 2001 From: Xuanwo Date: Mon, 22 Jul 2024 01:27:04 +0800 Subject: [PATCH 24/34] docs: Fix broken links in README.md (#4) Signed-off-by: Xuanwo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c278fc9..4fd4390 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

cloud-filter

Windows Cloud Sync

- docs + docs crates

From 4e3690d825858f6615f7dd7b84b67c0c7bbcbfbf Mon Sep 17 00:00:00 2001 From: Xuanwo Date: Mon, 22 Jul 2024 01:29:40 +0800 Subject: [PATCH 25/34] chore: Use windows target to build docs (#5) Signed-off-by: Xuanwo --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 41b1092..4c23496 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,9 @@ repository = "https://github.com/ho-229/cloud-filter-rs" documentation = "https://docs.rs/cloud-filter" exclude = ["examples/"] +[package.metadata.docs.rs] +default-target = "x86_64-pc-windows-msvc" + [dependencies] flagset = "0.4.5" widestring = "1.0.2" From f43705618aaf7f8a0ef02bb98b69419fa9b9888c Mon Sep 17 00:00:00 2001 From: Ho 229 <2189684957@qq.com> Date: Wed, 24 Jul 2024 01:53:46 +0800 Subject: [PATCH 26/34] feat: async filter --- Cargo.toml | 5 +- src/filter/async_filter.rs | 301 +++++++++++++++++++++++++++++++++++++ src/filter/mod.rs | 3 + src/filter/sync_filter.rs | 6 +- src/root/session.rs | 17 ++- src/utility.rs | 4 + 6 files changed, 330 insertions(+), 6 deletions(-) create mode 100644 src/filter/async_filter.rs diff --git a/Cargo.toml b/Cargo.toml index 4c23496..25368b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,10 @@ [package] name = "cloud-filter" version = "0.0.2" -authors = ["ok-nick ", "ho-229 "] +authors = [ + "ok-nick ", + "ho-229 ", +] edition = "2021" description = "A safe and idiomatic wrapper around the Windows Cloud Filter API" license = "MIT" diff --git a/src/filter/async_filter.rs b/src/filter/async_filter.rs new file mode 100644 index 0000000..774eea2 --- /dev/null +++ b/src/filter/async_filter.rs @@ -0,0 +1,301 @@ +use std::{future::Future, mem::MaybeUninit, path::PathBuf}; + +use crate::{ + error::{CResult, CloudErrorKind}, + request::Request, + utility::LocalBoxFuture, +}; + +use super::{info, ticket, SyncFilter}; + +/// Async core functions for implementing a Sync Engine. +/// +/// [Send] and [Sync] are required as the callback could be invoked from an arbitrary thread, [read +/// here](https://docs.microsoft.com/en-us/windows/win32/api/cfapi/ne-cfapi-cf_callback_type#remarks). +pub trait Filter: Send + Sync { + /// A placeholder hydration has been requested. This means that the placeholder should be + /// populated with its corresponding data on the remote. + fn fetch_data( + &self, + _request: Request, + _ticket: ticket::FetchData, + _info: info::FetchData, + ) -> impl Future> + Send + 'static; + + /// A placeholder hydration request has been cancelled. + fn cancel_fetch_data( + &self, + _request: Request, + _info: info::CancelFetchData, + ) -> impl Future + Send + 'static { + async {} + } + + /// Followed by a successful call to [Filter::fetch_data][super::Filter::fetch_data], this callback should verify the integrity of + /// the data persisted in the placeholder. + /// + /// **You** are responsible for validating the data in the placeholder. To approve + /// the request, use the ticket provided. + /// + /// Note that this callback is only called if [HydrationPolicy::ValidationRequired][crate::root::HydrationPolicy::ValidationRequired] + /// is specified. + fn validate_data( + &self, + _request: Request, + _ticket: ticket::ValidateData, + _info: info::ValidateData, + ) -> impl Future> + Send + 'static { + async { Err(CloudErrorKind::NotSupported) } + } + + /// A directory population has been requested. The behavior of this callback is dependent on + /// the [PopulationType][crate::root::PopulationType] variant specified during registration. + fn fetch_placeholders( + &self, + _request: Request, + _ticket: ticket::FetchPlaceholders, + _info: info::FetchPlaceholders, + ) -> impl Future> + Send + 'static { + async { Err(CloudErrorKind::NotSupported) } + } + + /// A directory population request has been cancelled. + fn cancel_fetch_placeholders( + &self, + _request: Request, + _info: info::CancelFetchPlaceholders, + ) -> impl Future + Send + 'static { + async {} + } + + /// A placeholder file handle has been opened for read, write, and/or delete + /// access. + fn opened( + &self, + _request: Request, + _info: info::Opened, + ) -> impl Future + Send + 'static { + async {} + } + + /// A placeholder file handle that has been previously opened with read, write, + /// and/or delete access has been closed. + fn closed( + &self, + _request: Request, + _info: info::Closed, + ) -> impl Future + Send + 'static { + async {} + } + + /// A placeholder dehydration has been requested. This means that all of the data persisted in + /// the file will be __completely__ discarded. + /// + /// The operating system will handle dehydrating placeholder files automatically. However, it + /// is up to **you** to approve this. Use the ticket to approve the request. + fn dehydrate( + &self, + _request: Request, + _ticket: ticket::Dehydrate, + _info: info::Dehydrate, + ) -> impl Future> + Send + 'static { + async { Err(CloudErrorKind::NotSupported) } + } + + /// A placeholder dehydration request has been cancelled. + fn dehydrated( + &self, + _request: Request, + _info: info::Dehydrated, + ) -> impl Future + Send + 'static { + async {} + } + + /// A placeholder file is about to be deleted. + /// + /// The operating system will handle deleting placeholder files automatically. However, it is + /// up to **you** to approve this. Use the ticket to approve the request. + fn delete( + &self, + _request: Request, + _ticket: ticket::Delete, + _info: info::Delete, + ) -> impl Future> + Send + 'static { + async { Err(CloudErrorKind::NotSupported) } + } + + /// A placeholder file has been deleted. + fn deleted( + &self, + _request: Request, + _info: info::Deleted, + ) -> impl Future + Send + 'static { + async {} + } + + /// A placeholder file is about to be renamed or moved. + /// + /// The operating system will handle moving and renaming placeholder files automatically. + /// However, it is up to **you** to approve this. Use the ticket to approve the + /// request. + /// + /// When the operation is completed, the [Filter::renamed] callback will be called. + fn rename( + &self, + _request: Request, + _ticket: ticket::Rename, + _info: info::Rename, + ) -> impl Future> + Send + 'static { + async { Err(CloudErrorKind::NotSupported) } + } + + /// A placeholder file has been renamed or moved. + fn renamed( + &self, + _request: Request, + _info: info::Renamed, + ) -> impl Future + Send + 'static { + async {} + } + + /// Placeholder for changed attributes under the sync root. + /// + /// This callback is implemented using [ReadDirectoryChangesW][https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw] + /// so it is not provided by the `Cloud Filter APIs`. + /// + /// This callback is used to detect when a user pins or unpins a placeholder file, etc. + /// + /// See also [Cloud Files API Frequently Asked Questions](https://www.userfilesystem.com/programming/faq/). + fn state_changed(&self, _changes: Vec) -> impl Future + Send + 'static { + async {} + } +} + +pub struct AsyncBridge { + filter: F, + block_on: B, +} + +impl AsyncBridge +where + F: Filter + Send + Sync + 'static, + B: Fn(LocalBoxFuture<'_, ()>), +{ + pub fn new(filter: F, block_on: B) -> Self { + Self { filter, block_on } + } +} + +impl SyncFilter for AsyncBridge +where + F: Filter + Send + Sync + 'static, + B: Fn(LocalBoxFuture<'_, ()>) + Send + Sync, +{ + fn fetch_data( + &self, + request: Request, + ticket: ticket::FetchData, + info: info::FetchData, + ) -> CResult<()> { + let mut ret = MaybeUninit::zeroed(); + (self.block_on)(Box::pin(async { + ret.write(self.filter.fetch_data(request, ticket, info).await); + })); + + unsafe { ret.assume_init() } + } + + fn cancel_fetch_data(&self, request: Request, info: info::CancelFetchData) { + (self.block_on)(Box::pin(self.filter.cancel_fetch_data(request, info))) + } + + fn validate_data( + &self, + request: Request, + ticket: ticket::ValidateData, + info: info::ValidateData, + ) -> CResult<()> { + let mut ret = MaybeUninit::zeroed(); + (self.block_on)(Box::pin(async { + ret.write(self.filter.validate_data(request, ticket, info).await); + })); + + unsafe { ret.assume_init() } + } + + fn fetch_placeholders( + &self, + request: Request, + ticket: ticket::FetchPlaceholders, + info: info::FetchPlaceholders, + ) -> CResult<()> { + let mut ret = MaybeUninit::zeroed(); + (self.block_on)(Box::pin(async { + ret.write(self.filter.fetch_placeholders(request, ticket, info).await); + })); + + unsafe { ret.assume_init() } + } + + fn cancel_fetch_placeholders(&self, request: Request, info: info::CancelFetchPlaceholders) { + (self.block_on)(Box::pin( + self.filter.cancel_fetch_placeholders(request, info), + )) + } + + fn opened(&self, request: Request, info: info::Opened) { + (self.block_on)(Box::pin(self.filter.opened(request, info))) + } + + fn closed(&self, request: Request, info: info::Closed) { + (self.block_on)(Box::pin(self.filter.closed(request, info))) + } + + fn dehydrate( + &self, + request: Request, + ticket: ticket::Dehydrate, + info: info::Dehydrate, + ) -> CResult<()> { + let mut ret = MaybeUninit::zeroed(); + (self.block_on)(Box::pin(async { + ret.write(self.filter.dehydrate(request, ticket, info).await); + })); + + unsafe { ret.assume_init() } + } + + fn dehydrated(&self, request: Request, info: info::Dehydrated) { + (self.block_on)(Box::pin(self.filter.dehydrated(request, info))) + } + + fn delete(&self, request: Request, ticket: ticket::Delete, info: info::Delete) -> CResult<()> { + let mut ret = MaybeUninit::zeroed(); + (self.block_on)(Box::pin(async { + ret.write(self.filter.delete(request, ticket, info).await); + })); + + unsafe { ret.assume_init() } + } + + fn deleted(&self, request: Request, info: info::Deleted) { + (self.block_on)(Box::pin(self.filter.deleted(request, info))) + } + + fn rename(&self, request: Request, ticket: ticket::Rename, info: info::Rename) -> CResult<()> { + let mut ret = MaybeUninit::zeroed(); + (self.block_on)(Box::pin(async { + ret.write(self.filter.rename(request, ticket, info).await); + })); + + unsafe { ret.assume_init() } + } + + fn renamed(&self, request: Request, info: info::Renamed) { + (self.block_on)(Box::pin(self.filter.renamed(request, info))) + } + + fn state_changed(&self, changes: Vec) { + (self.block_on)(Box::pin(self.filter.state_changed(changes))) + } +} diff --git a/src/filter/mod.rs b/src/filter/mod.rs index 0e21055..ae1322e 100644 --- a/src/filter/mod.rs +++ b/src/filter/mod.rs @@ -3,8 +3,11 @@ pub mod info; /// Tickets for callbacks in the [SyncFilter][crate::SyncFilter] trait. pub mod ticket; +pub(crate) use async_filter::AsyncBridge; +pub use async_filter::Filter; pub(crate) use proxy::{callbacks, Callbacks}; pub use sync_filter::SyncFilter; +mod async_filter; mod proxy; mod sync_filter; diff --git a/src/filter/sync_filter.rs b/src/filter/sync_filter.rs index 2400ba5..af4b6a6 100644 --- a/src/filter/sync_filter.rs +++ b/src/filter/sync_filter.rs @@ -18,9 +18,7 @@ pub trait SyncFilter: Send + Sync { _request: Request, _ticket: ticket::FetchData, _info: info::FetchData, - ) -> CResult<()> { - Err(CloudErrorKind::NotSupported) - } + ) -> CResult<()>; /// A placeholder hydration request has been cancelled. fn cancel_fetch_data(&self, _request: Request, _info: info::CancelFetchData) {} @@ -31,7 +29,7 @@ pub trait SyncFilter: Send + Sync { /// **You** are responsible for validating the data in the placeholder. To approve /// the request, use the ticket provided. /// - /// Note that this callback is only called if [HydrationPolicy::require_validation][crate::root::HydrationPolicy::require_validation] + /// Note that this callback is only called if [HydrationPolicy::ValidationRequired][crate::root::HydrationPolicy::ValidationRequired] /// is specified. fn validate_data( &self, diff --git a/src/root/session.rs b/src/root/session.rs index e440d35..07fc8f5 100644 --- a/src/root/session.rs +++ b/src/root/session.rs @@ -33,8 +33,9 @@ use windows::{ }; use crate::{ - filter::{self, SyncFilter}, + filter::{self, AsyncBridge, Filter, SyncFilter}, root::connect::Connection, + utility::LocalBoxFuture, }; /// A builder to create a new connection for the sync root at the specified path. @@ -99,6 +100,20 @@ impl Session { filter, )) } + + pub fn connect_async( + self, + path: P, + filter: F, + block_on: B, + ) -> core::Result>>> + where + P: AsRef, + F: Filter + 'static, + B: Fn(LocalBoxFuture<'_, ()>) + Send + Sync + 'static, + { + self.connect(path, AsyncBridge::new(filter, block_on)) + } } impl Default for Session { diff --git a/src/utility.rs b/src/utility.rs index e56ed70..bb2c0da 100644 --- a/src/utility.rs +++ b/src/utility.rs @@ -1,3 +1,5 @@ +use std::{future::Future, pin::Pin}; + use windows::core::{self, HSTRING}; use crate::sealed; @@ -29,3 +31,5 @@ pub trait ReadAt: sealed::Sealed { pub trait WriteAt: sealed::Sealed { fn write_at(&self, buf: &[u8], offset: u64) -> core::Result<()>; } + +pub(crate) type LocalBoxFuture<'a, T> = Pin + 'a>>; From 7235bd0fb57f74883da8ccba68ad5f904d31300e Mon Sep 17 00:00:00 2001 From: Ho 229 <2189684957@qq.com> Date: Wed, 24 Jul 2024 18:28:07 +0800 Subject: [PATCH 27/34] fix: async filter --- src/filter/async_filter.rs | 55 +++++++++++++------------------------- src/root/session.rs | 1 + 2 files changed, 19 insertions(+), 37 deletions(-) diff --git a/src/filter/async_filter.rs b/src/filter/async_filter.rs index 774eea2..f952641 100644 --- a/src/filter/async_filter.rs +++ b/src/filter/async_filter.rs @@ -20,14 +20,14 @@ pub trait Filter: Send + Sync { _request: Request, _ticket: ticket::FetchData, _info: info::FetchData, - ) -> impl Future> + Send + 'static; + ) -> impl Future>; /// A placeholder hydration request has been cancelled. fn cancel_fetch_data( &self, _request: Request, _info: info::CancelFetchData, - ) -> impl Future + Send + 'static { + ) -> impl Future { async {} } @@ -44,7 +44,7 @@ pub trait Filter: Send + Sync { _request: Request, _ticket: ticket::ValidateData, _info: info::ValidateData, - ) -> impl Future> + Send + 'static { + ) -> impl Future> { async { Err(CloudErrorKind::NotSupported) } } @@ -55,7 +55,7 @@ pub trait Filter: Send + Sync { _request: Request, _ticket: ticket::FetchPlaceholders, _info: info::FetchPlaceholders, - ) -> impl Future> + Send + 'static { + ) -> impl Future> { async { Err(CloudErrorKind::NotSupported) } } @@ -64,27 +64,19 @@ pub trait Filter: Send + Sync { &self, _request: Request, _info: info::CancelFetchPlaceholders, - ) -> impl Future + Send + 'static { + ) -> impl Future { async {} } /// A placeholder file handle has been opened for read, write, and/or delete /// access. - fn opened( - &self, - _request: Request, - _info: info::Opened, - ) -> impl Future + Send + 'static { + fn opened(&self, _request: Request, _info: info::Opened) -> impl Future { async {} } /// A placeholder file handle that has been previously opened with read, write, /// and/or delete access has been closed. - fn closed( - &self, - _request: Request, - _info: info::Closed, - ) -> impl Future + Send + 'static { + fn closed(&self, _request: Request, _info: info::Closed) -> impl Future { async {} } @@ -98,16 +90,12 @@ pub trait Filter: Send + Sync { _request: Request, _ticket: ticket::Dehydrate, _info: info::Dehydrate, - ) -> impl Future> + Send + 'static { + ) -> impl Future> { async { Err(CloudErrorKind::NotSupported) } } /// A placeholder dehydration request has been cancelled. - fn dehydrated( - &self, - _request: Request, - _info: info::Dehydrated, - ) -> impl Future + Send + 'static { + fn dehydrated(&self, _request: Request, _info: info::Dehydrated) -> impl Future { async {} } @@ -120,16 +108,12 @@ pub trait Filter: Send + Sync { _request: Request, _ticket: ticket::Delete, _info: info::Delete, - ) -> impl Future> + Send + 'static { + ) -> impl Future> { async { Err(CloudErrorKind::NotSupported) } } /// A placeholder file has been deleted. - fn deleted( - &self, - _request: Request, - _info: info::Deleted, - ) -> impl Future + Send + 'static { + fn deleted(&self, _request: Request, _info: info::Deleted) -> impl Future { async {} } @@ -145,16 +129,12 @@ pub trait Filter: Send + Sync { _request: Request, _ticket: ticket::Rename, _info: info::Rename, - ) -> impl Future> + Send + 'static { + ) -> impl Future> { async { Err(CloudErrorKind::NotSupported) } } /// A placeholder file has been renamed or moved. - fn renamed( - &self, - _request: Request, - _info: info::Renamed, - ) -> impl Future + Send + 'static { + fn renamed(&self, _request: Request, _info: info::Renamed) -> impl Future { async {} } @@ -166,11 +146,12 @@ pub trait Filter: Send + Sync { /// This callback is used to detect when a user pins or unpins a placeholder file, etc. /// /// See also [Cloud Files API Frequently Asked Questions](https://www.userfilesystem.com/programming/faq/). - fn state_changed(&self, _changes: Vec) -> impl Future + Send + 'static { + fn state_changed(&self, _changes: Vec) -> impl Future { async {} } } +/// Adapts a [Filter] to the [SyncFilter] trait. pub struct AsyncBridge { filter: F, block_on: B, @@ -178,8 +159,8 @@ pub struct AsyncBridge { impl AsyncBridge where - F: Filter + Send + Sync + 'static, - B: Fn(LocalBoxFuture<'_, ()>), + F: Filter, + B: Fn(LocalBoxFuture<'_, ()>) + Send + Sync, { pub fn new(filter: F, block_on: B) -> Self { Self { filter, block_on } @@ -188,7 +169,7 @@ where impl SyncFilter for AsyncBridge where - F: Filter + Send + Sync + 'static, + F: Filter, B: Fn(LocalBoxFuture<'_, ()>) + Send + Sync, { fn fetch_data( diff --git a/src/root/session.rs b/src/root/session.rs index 07fc8f5..4f2c449 100644 --- a/src/root/session.rs +++ b/src/root/session.rs @@ -101,6 +101,7 @@ impl Session { )) } + /// Initiates a connection to the sync root with the given [Filter]. pub fn connect_async( self, path: P, From 7a4ff53908596b84794aadb03c393c9b87525126 Mon Sep 17 00:00:00 2001 From: Ho 229 <2189684957@qq.com> Date: Wed, 24 Jul 2024 18:40:22 +0800 Subject: [PATCH 28/34] released 0.0.3 --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 25368b7..e18c410 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cloud-filter" -version = "0.0.2" +version = "0.0.3" authors = [ "ok-nick ", "ho-229 ", @@ -10,7 +10,7 @@ description = "A safe and idiomatic wrapper around the Windows Cloud Filter API" license = "MIT" repository = "https://github.com/ho-229/cloud-filter-rs" documentation = "https://docs.rs/cloud-filter" -exclude = ["examples/"] +exclude = ["examples/", ".github/"] [package.metadata.docs.rs] default-target = "x86_64-pc-windows-msvc" From c1a1275e479ec177ff87a3197062313a89af0a4c Mon Sep 17 00:00:00 2001 From: Ho 229 <2189684957@qq.com> Date: Sun, 28 Jul 2024 12:15:52 +0800 Subject: [PATCH 29/34] feat: bump `windows` to 0.58.0 (#6) --- Cargo.toml | 7 ++----- src/ext/file.rs | 2 +- src/root/sync_root_id.rs | 7 +++---- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e18c410..04008c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,25 +20,22 @@ flagset = "0.4.5" widestring = "1.0.2" nt-time = "0.8.0" memoffset = "0.9.1" -windows = { version = "0.52.0", features = [ +windows = { version = "0.58.0", features = [ "Win32_Foundation", "Win32_Storage_CloudFilters", - "Win32_System_SystemServices", "Win32_System_CorrelationVector", "Win32_Storage_FileSystem", "Win32_System_IO", "Storage_Provider", "Win32_System_Memory", "Storage", + "Storage_Search", "Foundation", "Foundation_Collections", "Win32_Security_Authorization", "Win32_UI_Shell", "Win32_System_Com", "Win32_UI_Shell_PropertiesSystem", - "Win32_System_Com_StructuredStorage", - "Win32_Storage_EnhancedStorage", - "Win32_System_Ole", "Win32_System_Search", "Storage_Streams", "Win32_System_Ioctl", diff --git a/src/ext/file.rs b/src/ext/file.rs index d4b5773..b6b2b49 100644 --- a/src/ext/file.rs +++ b/src/ext/file.rs @@ -47,7 +47,7 @@ fn dehydrate>( ) -> core::Result<()> { unsafe { CfDehydratePlaceholder( - HANDLE(handle as isize), + HANDLE(handle), match range.start_bound() { Bound::Included(x) => *x as i64, Bound::Excluded(x) => x.saturating_add(1) as i64, diff --git a/src/root/sync_root_id.rs b/src/root/sync_root_id.rs index 046024d..e935818 100644 --- a/src/root/sync_root_id.rs +++ b/src/root/sync_root_id.rs @@ -159,8 +159,7 @@ impl SyncRootId { if $info.$field().eq(OsStr::new("")) { Err(Error::new( ERROR_INVALID_PARAMETER.to_hresult(), - U16String::from_str(&concat!(stringify!($field), " cannot be empty")) - .to_hstring(), + concat!(stringify!($field), " cannot be empty"), ))?; } }; @@ -225,7 +224,7 @@ pub struct SecurityId(U16String); impl SecurityId { // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getcurrentthreadeffectivetoken - const CURRENT_THREAD_EFFECTIVE_TOKEN: HANDLE = HANDLE(-6); + const CURRENT_THREAD_EFFECTIVE_TOKEN: HANDLE = HANDLE(-6isize as *mut ::core::ffi::c_void); /// Creates a new [SecurityId] from [OsString]. /// @@ -273,7 +272,7 @@ impl SecurityId { ConvertSidToStringSidW(token.User.Sid, &mut sid as *mut _)?; let string_sid = U16CStr::from_ptr_str(sid.0).to_os_string(); - _ = LocalFree(HLOCAL(sid.0 as *mut _)); + LocalFree(HLOCAL(sid.0 as *mut _)); Ok(SecurityId::new(string_sid)) } From 13286c2ea802424ba2ecb82d089b2fe424309b3e Mon Sep 17 00:00:00 2001 From: Ho 229 <2189684957@qq.com> Date: Sun, 28 Jul 2024 17:30:28 +0800 Subject: [PATCH 30/34] chore: is_registered returns error when encounter unexpected error --- src/root/sync_root_id.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/root/sync_root_id.rs b/src/root/sync_root_id.rs index e935818..a438a04 100644 --- a/src/root/sync_root_id.rs +++ b/src/root/sync_root_id.rs @@ -136,12 +136,11 @@ impl SyncRootId { /// Whether or not the [SyncRootId] has already been registered. pub fn is_registered(&self) -> core::Result { - Ok( - match StorageProviderSyncRootManager::GetSyncRootInformationForId(&self.0) { - Ok(_) => true, - Err(err) => err.code() != Foundation::ERROR_NOT_FOUND.to_hresult(), - }, - ) + match StorageProviderSyncRootManager::GetSyncRootInformationForId(&self.0) { + Ok(_) => Ok(true), + Err(e) if e.code() == Foundation::ERROR_NOT_FOUND.to_hresult() => Ok(false), + Err(e) => Err(e), + } } /// Returns the sync root information for the [SyncRootId]. From 99cbd90dd7ec6130f23c44783e944b354711a0c0 Mon Sep 17 00:00:00 2001 From: Ho 229 Date: Sat, 3 Aug 2024 15:26:03 +0800 Subject: [PATCH 31/34] feat: behavior tests (#7) * feat: init behavior tests * feat: async filter behavior test * fix: format and lint * ci: setup test behavior * fix: ci * fix: ci --- .github/workflows/check.yml | 1 + .github/workflows/test_behavior.yml | 24 +++++ Cargo.toml | 13 ++- src/filter/async_filter.rs | 16 +++- src/filter/mod.rs | 3 +- src/root/connect.rs | 8 +- src/root/session.rs | 8 +- tests/behavior/async_filter.rs | 144 ++++++++++++++++++++++++++++ tests/behavior/main.rs | 41 ++++++++ tests/behavior/sync_filter.rs | 139 +++++++++++++++++++++++++++ 10 files changed, 384 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/test_behavior.yml create mode 100644 tests/behavior/async_filter.rs create mode 100644 tests/behavior/main.rs create mode 100644 tests/behavior/sync_filter.rs diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 70a680f..f8fec00 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -1,4 +1,5 @@ name: check + on: workflow_dispatch: push: diff --git a/.github/workflows/test_behavior.yml b/.github/workflows/test_behavior.yml new file mode 100644 index 0000000..4b908af --- /dev/null +++ b/.github/workflows/test_behavior.yml @@ -0,0 +1,24 @@ +name: test behavior + +on: + workflow_dispatch: + push: + paths: + - "**.rs" + - "**/Cargo.toml" + - ".github/workflows/test_behavior.yml" + pull_request: + paths: + - "**.rs" + - "**/Cargo.toml" + - ".github/workflows/test_behavior.yml" + +jobs: + test: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + + - name: cargo test + run: cargo test behavior --all-features diff --git a/Cargo.toml b/Cargo.toml index 04008c9..98c2143 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ description = "A safe and idiomatic wrapper around the Windows Cloud Filter API" license = "MIT" repository = "https://github.com/ho-229/cloud-filter-rs" documentation = "https://docs.rs/cloud-filter" -exclude = ["examples/", ".github/"] +exclude = ["examples/", ".github/", "test/"] [package.metadata.docs.rs] default-target = "x86_64-pc-windows-msvc" @@ -43,6 +43,12 @@ windows = { version = "0.58.0", features = [ ] } globset = { version = "0.4.9", optional = true } +[dev-dependencies] +libtest-mimic = "0.7.3" +futures = "0.3.30" +anyhow = "1.0.86" +powershell_script = "1.1.0" + [features] # Enable globs in the `info::FetchPlaceholders` struct. globs = ["globset"] @@ -50,3 +56,8 @@ globs = ["globset"] # TODO: temporarily ignored [workspace] members = ["examples/sftp"] + +[[test]] +harness = false +name = "behavior" +path = "tests/behavior/main.rs" diff --git a/src/filter/async_filter.rs b/src/filter/async_filter.rs index f952641..e07ba53 100644 --- a/src/filter/async_filter.rs +++ b/src/filter/async_filter.rs @@ -1,4 +1,4 @@ -use std::{future::Future, mem::MaybeUninit, path::PathBuf}; +use std::{future::Future, mem::MaybeUninit, ops::Deref, path::PathBuf}; use crate::{ error::{CResult, CloudErrorKind}, @@ -162,7 +162,7 @@ where F: Filter, B: Fn(LocalBoxFuture<'_, ()>) + Send + Sync, { - pub fn new(filter: F, block_on: B) -> Self { + pub(crate) fn new(filter: F, block_on: B) -> Self { Self { filter, block_on } } } @@ -280,3 +280,15 @@ where (self.block_on)(Box::pin(self.filter.state_changed(changes))) } } + +impl Deref for AsyncBridge +where + F: Filter, + B: Fn(LocalBoxFuture<'_, ()>) + Send + Sync, +{ + type Target = F; + + fn deref(&self) -> &Self::Target { + &self.filter + } +} diff --git a/src/filter/mod.rs b/src/filter/mod.rs index ae1322e..433cd34 100644 --- a/src/filter/mod.rs +++ b/src/filter/mod.rs @@ -3,8 +3,7 @@ pub mod info; /// Tickets for callbacks in the [SyncFilter][crate::SyncFilter] trait. pub mod ticket; -pub(crate) use async_filter::AsyncBridge; -pub use async_filter::Filter; +pub use async_filter::{AsyncBridge, Filter}; pub(crate) use proxy::{callbacks, Callbacks}; pub use sync_filter::SyncFilter; diff --git a/src/root/connect.rs b/src/root/connect.rs index 23dd0dc..849290b 100644 --- a/src/root/connect.rs +++ b/src/root/connect.rs @@ -1,5 +1,5 @@ use std::{ - sync::mpsc::Sender, + sync::{mpsc::Sender, Arc}, thread::{self, JoinHandle}, time::Duration, }; @@ -14,14 +14,14 @@ use crate::{filter::Callbacks, request::RawConnectionKey}; /// does **NOT** mean the sync root will be unregistered. To do so, call /// [SyncRootId::unregister][crate::root::SyncRootId::unregister]. #[derive(Debug)] -pub struct Connection { +pub struct Connection { connection_key: RawConnectionKey, cancel_token: Sender<()>, join_handle: JoinHandle<()>, _callbacks: Callbacks, - filter: T, + filter: Arc, } // this struct could house many more windows api functions, although they all seem to do nothing @@ -32,7 +32,7 @@ impl Connection { cancel_token: Sender<()>, join_handle: JoinHandle<()>, callbacks: Callbacks, - filter: T, + filter: Arc, ) -> Self { Self { connection_key, diff --git a/src/root/session.rs b/src/root/session.rs index 4f2c449..7b300c3 100644 --- a/src/root/session.rs +++ b/src/root/session.rs @@ -60,16 +60,16 @@ impl Session { } /// Initiates a connection to the sync root with the given [SyncFilter]. - pub fn connect(self, path: P, filter: T) -> core::Result>> + pub fn connect(self, path: P, filter: F) -> core::Result> where P: AsRef, - T: SyncFilter + 'static, + F: SyncFilter + 'static, { // https://github.com/microsoft/Windows-classic-samples/blob/27ffb0811ca761741502feaefdb591aebf592193/Samples/CloudMirror/CloudMirror/Utilities.cpp#L19 index_path(path.as_ref())?; let filter = Arc::new(filter); - let callbacks = filter::callbacks::(); + let callbacks = filter::callbacks::(); let key = unsafe { CfConnectSyncRoot( PCWSTR( @@ -107,7 +107,7 @@ impl Session { path: P, filter: F, block_on: B, - ) -> core::Result>>> + ) -> core::Result>> where P: AsRef, F: Filter + 'static, diff --git a/tests/behavior/async_filter.rs b/tests/behavior/async_filter.rs new file mode 100644 index 0000000..39fdefd --- /dev/null +++ b/tests/behavior/async_filter.rs @@ -0,0 +1,144 @@ +use core::str; +use std::{fs, future::Future, path::Path, pin::Pin}; + +use anyhow::Context; +use cloud_filter::{ + error::{CResult, CloudErrorKind}, + filter::{info, ticket, AsyncBridge, Filter}, + metadata::Metadata, + placeholder_file::PlaceholderFile, + request::Request, + root::{ + Connection, HydrationType, PopulationType, SecurityId, Session, SyncRootId, + SyncRootIdBuilder, SyncRootInfo, + }, + utility::WriteAt, +}; +use libtest_mimic::Failed; +use nt_time::FileTime; + +const ROOT_PATH: &str = "C:\\async_filter_test"; + +struct MemFilter; + +impl Filter for MemFilter { + async fn fetch_data( + &self, + request: Request, + ticket: ticket::FetchData, + info: info::FetchData, + ) -> CResult<()> { + let path = unsafe { str::from_utf8_unchecked(request.file_blob()) }; + println!("fetch_data: path: {path:?}"); + + let content = match path.as_ref() { + "test1.txt" | "dir1\\test2.txt" => path, + _ => Err(CloudErrorKind::InvalidRequest)?, + }; + + if info.required_file_range() != (0..content.len() as u64) { + Err(CloudErrorKind::InvalidRequest)?; + } + + ticket.write_at(content.as_bytes(), 0).unwrap(); + + Ok(()) + } + + async fn fetch_placeholders( + &self, + request: Request, + ticket: ticket::FetchPlaceholders, + _info: info::FetchPlaceholders, + ) -> CResult<()> { + let path = request.path(); + let relative_path = path.strip_prefix(ROOT_PATH).unwrap(); + println!("fetch_placeholders: path: {path:?}, relative path: {relative_path:?}"); + + let now = FileTime::now(); + let mut placeholders = match relative_path.to_string_lossy().as_ref() { + "" => vec![ + PlaceholderFile::new("dir1") + .mark_in_sync() + .metadata(Metadata::directory().created(now).written(now).size(0)) + .blob("dir1".into()), + PlaceholderFile::new("test1.txt") + .has_no_children() + .mark_in_sync() + .metadata( + Metadata::file() + .created(now) + .written(now) + .size("test1.txt".len() as _), + ) + .blob("test1.txt".into()), + ], + "dir1" => vec![PlaceholderFile::new("test2.txt") + .has_no_children() + .mark_in_sync() + .metadata( + Metadata::file() + .created(now) + .written(now) + .size("dir1\\test2.txt".len() as _), + ) + .blob("dir1\\test2.txt".into())], + _ => Err(CloudErrorKind::InvalidRequest)?, + }; + + ticket.pass_with_placeholder(&mut placeholders).unwrap(); + Ok(()) + } +} + +fn init() -> anyhow::Result<( + SyncRootId, + Connection>>)>>, +)> { + let sync_root_id = SyncRootIdBuilder::new("sync_filter_test_provider") + .user_security_id(SecurityId::current_user().context("current_user")?) + .build(); + + if !sync_root_id.is_registered().context("is_registered")? { + sync_root_id + .register( + SyncRootInfo::default() + .with_display_name("Sync Filter Test") + .with_hydration_type(HydrationType::Full) + .with_population_type(PopulationType::Full) + .with_icon("%SystemRoot%\\system32\\charmap.exe,0") + .with_version("1.0.0") + .with_recycle_bin_uri("http://cloudmirror.example.com/recyclebin") + .context("recycle_bin_uri")? + .with_path(ROOT_PATH) + .context("path")?, + ) + .context("register")? + } + + let connection = Session::new() + .connect_async(ROOT_PATH, MemFilter, move |f| { + futures::executor::block_on(f) + }) + .context("connect")?; + + Ok((sync_root_id, connection)) +} + +pub fn test() -> Result<(), Failed> { + if !Path::new(ROOT_PATH).try_exists().context("exists")? { + fs::create_dir(ROOT_PATH).context("create root dir")?; + } + + let (sync_root_id, connection) = init().context("init")?; + + crate::test_list_folders(ROOT_PATH); + crate::test_read_file(ROOT_PATH); + + drop(connection); + sync_root_id.unregister().context("unregister")?; + + fs::remove_dir_all(ROOT_PATH).context("remove root dir")?; + + Ok(()) +} diff --git a/tests/behavior/main.rs b/tests/behavior/main.rs new file mode 100644 index 0000000..440f3a8 --- /dev/null +++ b/tests/behavior/main.rs @@ -0,0 +1,41 @@ +use std::process::ExitCode; + +use libtest_mimic::{run, Arguments, Trial}; + +mod async_filter; +mod sync_filter; + +fn main() -> ExitCode { + let args = Arguments::from_args(); + let tests = vec![Trial::test("sync_filter", sync_filter::test)]; + + let conclusion = run(&args, tests); + if conclusion.has_failed() { + return conclusion.exit_code(); + } + + let tests = vec![Trial::test("async_filter", async_filter::test)]; + let conclusion = run(&args, tests); + + conclusion.exit_code() +} + +fn test_list_folders(root: &str) { + let output = powershell_script::run(&format!("Get-ChildItem {root} -Recurse -Name")) + .expect("run script"); + assert_eq!( + "dir1\r\n\ + test1.txt\r\n\ + dir1\\test2.txt\r\n", + output.stdout().expect("stdout"), + ); +} + +fn test_read_file(root: &str) { + for relative in ["test1.txt", "dir1\\test2.txt"] { + let path = format!("{root}\\{relative}"); + let output = + powershell_script::run(&format!("Get-Content {path} -Raw")).expect("run script"); + assert_eq!(output.stdout().expect("stdout"), format!("{relative}\r\n")); + } +} diff --git a/tests/behavior/sync_filter.rs b/tests/behavior/sync_filter.rs new file mode 100644 index 0000000..81e4ab7 --- /dev/null +++ b/tests/behavior/sync_filter.rs @@ -0,0 +1,139 @@ +use core::str; +use std::{fs, path::Path}; + +use anyhow::Context; +use cloud_filter::{ + error::{CResult, CloudErrorKind}, + filter::{info, ticket, SyncFilter}, + metadata::Metadata, + placeholder_file::PlaceholderFile, + request::Request, + root::{ + Connection, HydrationType, PopulationType, SecurityId, Session, SyncRootId, + SyncRootIdBuilder, SyncRootInfo, + }, + utility::WriteAt, +}; +use libtest_mimic::Failed; +use nt_time::FileTime; + +const ROOT_PATH: &str = "C:\\sync_filter_test"; + +struct MemFilter; + +impl SyncFilter for MemFilter { + fn fetch_data( + &self, + request: Request, + ticket: ticket::FetchData, + info: info::FetchData, + ) -> CResult<()> { + let path = unsafe { str::from_utf8_unchecked(request.file_blob()) }; + println!("fetch_data: path: {path:?}"); + + let content = match path.as_ref() { + "test1.txt" | "dir1\\test2.txt" => path, + _ => Err(CloudErrorKind::InvalidRequest)?, + }; + + if info.required_file_range() != (0..content.len() as u64) { + Err(CloudErrorKind::InvalidRequest)?; + } + + ticket.write_at(content.as_bytes(), 0).unwrap(); + + Ok(()) + } + + fn fetch_placeholders( + &self, + request: Request, + ticket: ticket::FetchPlaceholders, + _info: info::FetchPlaceholders, + ) -> CResult<()> { + let path = request.path(); + let relative_path = path.strip_prefix(ROOT_PATH).unwrap(); + println!("fetch_placeholders: path: {path:?}, relative path: {relative_path:?}"); + + let now = FileTime::now(); + let mut placeholders = match relative_path.to_string_lossy().as_ref() { + "" => vec![ + PlaceholderFile::new("dir1") + .mark_in_sync() + .metadata(Metadata::directory().created(now).written(now).size(0)) + .blob("dir1".into()), + PlaceholderFile::new("test1.txt") + .has_no_children() + .mark_in_sync() + .metadata( + Metadata::file() + .created(now) + .written(now) + .size("test1.txt".len() as _), + ) + .blob("test1.txt".into()), + ], + "dir1" => vec![PlaceholderFile::new("test2.txt") + .has_no_children() + .mark_in_sync() + .metadata( + Metadata::file() + .created(now) + .written(now) + .size("dir1\\test2.txt".len() as _), + ) + .blob("dir1\\test2.txt".into())], + _ => Err(CloudErrorKind::InvalidRequest)?, + }; + + ticket.pass_with_placeholder(&mut placeholders).unwrap(); + Ok(()) + } +} + +fn init() -> anyhow::Result<(SyncRootId, Connection)> { + let sync_root_id = SyncRootIdBuilder::new("sync_filter_test_provider") + .user_security_id(SecurityId::current_user().context("current_user")?) + .build(); + + if !sync_root_id.is_registered().context("is_registered")? { + sync_root_id + .register( + SyncRootInfo::default() + .with_display_name("Sync Filter Test") + .with_hydration_type(HydrationType::Full) + .with_population_type(PopulationType::Full) + .with_icon("%SystemRoot%\\system32\\charmap.exe,0") + .with_version("1.0.0") + .with_recycle_bin_uri("http://cloudmirror.example.com/recyclebin") + .context("recycle_bin_uri")? + .with_path(ROOT_PATH) + .context("path")?, + ) + .context("register")? + } + + let connection = Session::new() + .connect(ROOT_PATH, MemFilter) + .context("connect")?; + + Ok((sync_root_id, connection)) +} + +pub fn test() -> Result<(), Failed> { + if !Path::new(ROOT_PATH).try_exists().context("exists")? { + fs::create_dir(ROOT_PATH).context("create root dir")?; + } + + let (sync_root_id, connection) = init().context("init")?; + + crate::test_list_folders(ROOT_PATH); + crate::test_read_file(ROOT_PATH); + + drop(connection); + sync_root_id.unregister().context("unregister")?; + + fs::remove_dir_all(ROOT_PATH).context("remove root dir")?; + + Ok(()) +} From 8eb00a80d757b24c2051496394738a55d2d55514 Mon Sep 17 00:00:00 2001 From: Ho 229 <2189684957@qq.com> Date: Sat, 10 Aug 2024 23:40:14 +0800 Subject: [PATCH 32/34] docs: lib.rs --- src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index ca806b3..5079d1f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#![doc = "../README.md"] + pub mod error; /// Contains traits extending common structs from the [std][std]. pub mod ext; From 682a37f357fbc551e19bd5ba5a45df17e5431aad Mon Sep 17 00:00:00 2001 From: Ho 229 <2189684957@qq.com> Date: Sat, 10 Aug 2024 23:41:17 +0800 Subject: [PATCH 33/34] released 0.0.4 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 98c2143..3165b71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cloud-filter" -version = "0.0.3" +version = "0.0.4" authors = [ "ok-nick ", "ho-229 ", From 5a9e255d1edcb235c2172bcc3eb28a527d6bd286 Mon Sep 17 00:00:00 2001 From: Ho 229 <2189684957@qq.com> Date: Sun, 11 Aug 2024 00:08:39 +0800 Subject: [PATCH 34/34] fix: behavior test ci --- .github/workflows/test_behavior.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_behavior.yml b/.github/workflows/test_behavior.yml index 4b908af..a0857fd 100644 --- a/.github/workflows/test_behavior.yml +++ b/.github/workflows/test_behavior.yml @@ -21,4 +21,4 @@ jobs: - uses: dtolnay/rust-toolchain@stable - name: cargo test - run: cargo test behavior --all-features + run: cargo test --test behavior --all-features