#![forbid(unsafe_code)]
use super::{
constants,
{seq_iter::SeqIter, visitor::impl_visitor},
};
use std::{
convert::TryInto,
time::{Duration, SystemTime},
};
use bitflags::bitflags;
use num_derive::FromPrimitive;
use num_traits::cast::FromPrimitive;
use openssh_sftp_protocol_error::UnixTimeStampError;
use serde::{
de::{Error, Unexpected},
ser::{SerializeTuple, Serializer},
Serialize,
};
const S_IFMT: u32 = 0o170000;
bitflags! {
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
struct FileAttrsFlags: u8 {
const SIZE = 1 << 0;
const ID = 1 << 1;
const PERMISSIONS = 1 << 2;
const TIME = 1 << 3;
const EXTENSIONS = 1 << 4;
}
}
impl Serialize for FileAttrsFlags {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use constants::{
SSH_FILEXFER_ATTR_ACMODTIME, SSH_FILEXFER_ATTR_PERMISSIONS, SSH_FILEXFER_ATTR_SIZE,
SSH_FILEXFER_ATTR_UIDGID,
};
let mut flags: u32 = 0;
if self.intersects(FileAttrsFlags::SIZE) {
flags |= SSH_FILEXFER_ATTR_SIZE;
}
if self.intersects(FileAttrsFlags::ID) {
flags |= SSH_FILEXFER_ATTR_UIDGID;
}
if self.intersects(FileAttrsFlags::PERMISSIONS) {
flags |= SSH_FILEXFER_ATTR_PERMISSIONS;
}
if self.intersects(FileAttrsFlags::TIME) {
flags |= SSH_FILEXFER_ATTR_ACMODTIME;
}
flags.serialize(serializer)
}
}
impl<'de> crate::visitor::Deserialize<'de> for FileAttrsFlags {
fn deserialize<D: crate::visitor::Deserializer<'de>>(
deserializer: D,
) -> Result<Self, D::Error> {
use constants::{
SSH_FILEXFER_ATTR_ACMODTIME, SSH_FILEXFER_ATTR_EXTENDED, SSH_FILEXFER_ATTR_PERMISSIONS,
SSH_FILEXFER_ATTR_SIZE, SSH_FILEXFER_ATTR_UIDGID,
};
let flags = u32::deserialize(deserializer)?;
let has_attr = |attr_mask| -> bool { (flags & attr_mask) != 0 };
let mut file_attrs_flags = FileAttrsFlags::empty();
if has_attr(SSH_FILEXFER_ATTR_SIZE) {
file_attrs_flags |= FileAttrsFlags::SIZE;
}
if has_attr(SSH_FILEXFER_ATTR_UIDGID) {
file_attrs_flags |= FileAttrsFlags::ID;
}
if has_attr(SSH_FILEXFER_ATTR_PERMISSIONS) {
file_attrs_flags |= FileAttrsFlags::PERMISSIONS;
}
if has_attr(SSH_FILEXFER_ATTR_ACMODTIME) {
file_attrs_flags |= FileAttrsFlags::TIME;
}
if has_attr(SSH_FILEXFER_ATTR_EXTENDED) {
file_attrs_flags |= FileAttrsFlags::EXTENSIONS;
}
Ok(file_attrs_flags)
}
}
bitflags! {
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Permissions: u32 {
const SET_UID = 0o4000;
const SET_GID = 0o2000;
const SET_VTX = 0o1000;
const READ_BY_OWNER = 0o400;
const WRITE_BY_OWNER = 0o200;
const EXECUTE_BY_OWNER = 0o100;
const READ_BY_GROUP = 0o40;
const WRITE_BY_GROUP = 0o20;
const EXECUTE_BY_GROUP = 0o10;
const READ_BY_OTHER = 0o4;
const WRITE_BY_OTHER = 0o2;
const EXECUTE_BY_OTHER = 0o1;
}
}
#[derive(Debug, Clone, Copy, FromPrimitive, Eq, PartialEq, Hash)]
#[repr(u32)]
pub enum FileType {
Socket = 0o140000,
Symlink = 0o120000,
RegularFile = 0o100000,
BlockDevice = 0o60000,
Directory = 0o40000,
CharacterDevice = 0o20000,
FIFO = 0o10000,
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct UnixTimeStamp(u32);
impl UnixTimeStamp {
pub fn new(system_time: SystemTime) -> Result<Self, UnixTimeStampError> {
let duration = system_time.duration_since(SystemTime::UNIX_EPOCH)?;
let seconds: u32 = duration.as_secs().try_into()?;
Ok(Self(seconds))
}
pub const fn unix_epoch() -> Self {
Self(0)
}
pub fn from_raw(elapsed: u32) -> Option<Self> {
let this = Self(elapsed);
let duration = this.as_duration();
SystemTime::UNIX_EPOCH.checked_add(duration)?;
Some(this)
}
pub fn into_raw(self) -> u32 {
self.0
}
pub fn as_duration(self) -> Duration {
Duration::from_secs(self.0 as u64)
}
pub fn as_system_time(self) -> SystemTime {
SystemTime::UNIX_EPOCH + self.as_duration()
}
}
impl_visitor!(
UnixTimeStamp,
UnixTimeStampVisitor,
"Unix Timestamp",
seq,
{
let mut iter = SeqIter::new(seq);
let elapsed: u32 = iter.get_next()?;
let timestamp = UnixTimeStamp::from_raw(elapsed).ok_or_else(|| {
V::Error::invalid_value(
Unexpected::Unsigned(elapsed as u64),
&"Invalid UnixTimeStamp (seconds)",
)
})?;
Ok(timestamp)
}
);
#[derive(Debug, Default, Copy, Clone)]
pub struct FileAttrs {
flags: FileAttrsFlags,
size: u64,
uid: u32,
gid: u32,
st_mode: u32,
atime: UnixTimeStamp,
mtime: UnixTimeStamp,
}
impl PartialEq for FileAttrs {
fn eq(&self, other: &Self) -> bool {
self.get_size() == other.get_size()
&& self.get_id() == other.get_id()
&& self.get_permissions() == other.get_permissions()
&& self.get_filetype() == other.get_filetype()
&& self.get_time() == other.get_time()
}
}
impl Eq for FileAttrs {}
impl FileAttrs {
pub const fn new() -> Self {
Self {
flags: FileAttrsFlags::empty(),
size: 0,
uid: 0,
gid: 0,
st_mode: 0,
atime: UnixTimeStamp::unix_epoch(),
mtime: UnixTimeStamp::unix_epoch(),
}
}
pub fn set_size(&mut self, size: u64) {
self.flags |= FileAttrsFlags::SIZE;
self.size = size;
}
pub fn set_id(&mut self, uid: u32, gid: u32) {
self.flags |= FileAttrsFlags::ID;
self.uid = uid;
self.gid = gid;
}
pub fn set_permissions(&mut self, permissions: Permissions) {
self.flags |= FileAttrsFlags::PERMISSIONS;
let filetype = self.st_mode & S_IFMT;
self.st_mode = filetype | permissions.bits();
}
pub fn set_time(&mut self, atime: UnixTimeStamp, mtime: UnixTimeStamp) {
self.flags |= FileAttrsFlags::TIME;
self.atime = atime;
self.mtime = mtime;
}
fn has_attr(&self, flag: FileAttrsFlags) -> bool {
self.flags.intersects(flag)
}
fn getter_impl<T>(&self, flag: FileAttrsFlags, f: impl FnOnce() -> T) -> Option<T> {
if self.has_attr(flag) {
Some(f())
} else {
None
}
}
pub fn get_size(&self) -> Option<u64> {
self.getter_impl(FileAttrsFlags::SIZE, || self.size)
}
pub fn get_id(&self) -> Option<(u32, u32)> {
self.getter_impl(FileAttrsFlags::ID, || (self.uid, self.gid))
}
pub fn get_permissions(&self) -> Option<Permissions> {
self.getter_impl(FileAttrsFlags::PERMISSIONS, || {
Permissions::from_bits_truncate(self.st_mode)
})
}
pub fn get_filetype(&self) -> Option<FileType> {
self.getter_impl(FileAttrsFlags::PERMISSIONS, || {
let filetype = self.st_mode & S_IFMT;
if filetype == 0 {
None
} else {
Some(FileType::from_u32(filetype).unwrap())
}
})
.flatten()
}
pub fn get_time(&self) -> Option<(UnixTimeStamp, UnixTimeStamp)> {
self.getter_impl(FileAttrsFlags::TIME, || (self.atime, self.mtime))
}
}
impl Serialize for FileAttrs {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut tuple_serializer = serializer.serialize_tuple(1)?;
tuple_serializer.serialize_element(&self.flags)?;
if let Some(size) = self.get_size() {
tuple_serializer.serialize_element(&size)?;
}
if let Some((uid, gid)) = self.get_id() {
tuple_serializer.serialize_element(&uid)?;
tuple_serializer.serialize_element(&gid)?;
}
if let Some(perm) = self.get_permissions() {
tuple_serializer.serialize_element(&perm.bits())?;
}
if let Some((atime, mtime)) = self.get_time() {
tuple_serializer.serialize_element(&atime.into_raw())?;
tuple_serializer.serialize_element(&mtime.into_raw())?;
}
tuple_serializer.end()
}
}
impl_visitor!(FileAttrs, FileAttrVisitor, "File attributes", seq, {
let mut iter = SeqIter::new(seq);
let mut attrs = FileAttrs {
flags: iter.get_next()?,
..Default::default()
};
if attrs.has_attr(FileAttrsFlags::SIZE) {
attrs.size = iter.get_next()?;
}
if attrs.has_attr(FileAttrsFlags::ID) {
attrs.uid = iter.get_next()?;
attrs.gid = iter.get_next()?;
}
if attrs.has_attr(FileAttrsFlags::PERMISSIONS) {
attrs.st_mode = iter.get_next()?;
let filetype = attrs.st_mode & S_IFMT;
if filetype != 0 && FileType::from_u32(filetype).is_none() {
return Err(V::Error::invalid_value(
Unexpected::Unsigned(filetype as u64),
&"Expected valid filetype specified in POSIX",
));
}
}
let into_timestamp = |elapsed: u32| {
let timestamp = UnixTimeStamp::from_raw(elapsed).ok_or_else(|| {
V::Error::invalid_value(
Unexpected::Unsigned(elapsed as u64),
&"Invalid UnixTimeStamp (seconds)",
)
})?;
Ok(timestamp)
};
if attrs.has_attr(FileAttrsFlags::TIME) {
attrs.atime = into_timestamp(iter.get_next()?)?;
attrs.mtime = into_timestamp(iter.get_next()?)?;
}
if attrs.has_attr(FileAttrsFlags::EXTENSIONS) {
let extension_pairs: u32 = iter.get_next()?;
for _i in 0..extension_pairs {
let _name: &[u8] = iter.get_next()?;
let _value: &[u8] = iter.get_next()?;
}
}
attrs.flags.remove(FileAttrsFlags::EXTENSIONS);
Ok(attrs)
});
#[cfg(test)]
mod tests {
use super::{FileAttrs, FileAttrsFlags, FileType, Permissions, UnixTimeStamp};
use super::constants::{
SSH_FILEXFER_ATTR_ACMODTIME, SSH_FILEXFER_ATTR_PERMISSIONS, SSH_FILEXFER_ATTR_SIZE,
SSH_FILEXFER_ATTR_UIDGID,
};
fn get_unix_timestamps() -> (UnixTimeStamp, UnixTimeStamp) {
(
UnixTimeStamp::from_raw(2).unwrap(),
UnixTimeStamp::from_raw(150).unwrap(),
)
}
#[test]
fn test_set_get_size() {
let mut attrs = FileAttrs::default();
attrs.set_size(2333);
assert_eq!(attrs.get_size().unwrap(), 2333);
}
#[test]
fn test_set_get_id() {
let mut attrs = FileAttrs::default();
attrs.set_id(u32::MAX, 1000);
assert_eq!(attrs.get_id().unwrap(), (u32::MAX, 1000));
}
#[test]
fn test_set_get_permissions() {
let mut attrs = FileAttrs::default();
attrs.set_permissions(Permissions::SET_GID);
assert_eq!(attrs.get_permissions().unwrap(), Permissions::SET_GID);
}
#[test]
fn test_set_get_time() {
let (atime, mtime) = get_unix_timestamps();
let mut attrs = FileAttrs::default();
attrs.set_time(atime, mtime);
assert_eq!(attrs.get_time().unwrap(), (atime, mtime));
}
use serde_test::{assert_de_tokens, assert_tokens, Token};
#[test]
fn test_file_attr_flags() {
assert_tokens(&FileAttrsFlags::empty(), &[Token::U32(0)]);
assert_tokens(&FileAttrsFlags::SIZE, &[Token::U32(SSH_FILEXFER_ATTR_SIZE)]);
assert_tokens(&FileAttrsFlags::ID, &[Token::U32(SSH_FILEXFER_ATTR_UIDGID)]);
assert_tokens(
&FileAttrsFlags::PERMISSIONS,
&[Token::U32(SSH_FILEXFER_ATTR_PERMISSIONS)],
);
assert_tokens(
&FileAttrsFlags::TIME,
&[Token::U32(SSH_FILEXFER_ATTR_ACMODTIME)],
);
}
fn init_attrs(f: impl FnOnce(&mut FileAttrs)) -> FileAttrs {
let mut attrs = FileAttrs::default();
f(&mut attrs);
attrs
}
#[test]
fn test_ser_de_size() {
assert_tokens(
&init_attrs(|attrs| attrs.set_size(2333)),
&[
Token::Tuple { len: 1 },
Token::U32(SSH_FILEXFER_ATTR_SIZE),
Token::U64(2333),
Token::TupleEnd,
],
);
}
#[test]
fn test_ser_de_id() {
assert_tokens(
&init_attrs(|attrs| attrs.set_id(u32::MAX, 1000)),
&[
Token::Tuple { len: 1 },
Token::U32(SSH_FILEXFER_ATTR_UIDGID),
Token::U32(u32::MAX),
Token::U32(1000),
Token::TupleEnd,
],
);
}
#[test]
fn test_ser_de_permissions_and_filetype() {
assert_tokens(
&init_attrs(|attrs| attrs.set_permissions(Permissions::WRITE_BY_OTHER)),
&[
Token::Tuple { len: 1 },
Token::U32(SSH_FILEXFER_ATTR_PERMISSIONS),
Token::U32(Permissions::WRITE_BY_OTHER.bits()),
Token::TupleEnd,
],
);
assert_de_tokens(
&init_attrs(|attrs| {
attrs.set_permissions(Permissions::WRITE_BY_OTHER);
attrs.st_mode = Permissions::WRITE_BY_OTHER.bits() | FileType::Socket as u32;
}),
&[
Token::Tuple { len: 1 },
Token::U32(SSH_FILEXFER_ATTR_PERMISSIONS),
Token::U32(Permissions::WRITE_BY_OTHER.bits() | FileType::Socket as u32),
Token::TupleEnd,
],
);
}
#[test]
fn test_ser_de_time() {
let (atime, mtime) = get_unix_timestamps();
assert_tokens(
&init_attrs(|attrs| attrs.set_time(atime, mtime)),
&[
Token::Tuple { len: 1 },
Token::U32(SSH_FILEXFER_ATTR_ACMODTIME),
Token::U32(atime.into_raw()),
Token::U32(mtime.into_raw()),
Token::TupleEnd,
],
);
}
#[test]
fn test_ser_de_all() {
let (atime, mtime) = get_unix_timestamps();
assert_tokens(
&init_attrs(|attrs| {
attrs.set_size(2333);
attrs.set_id(u32::MAX, 1000);
attrs.set_permissions(Permissions::READ_BY_OWNER);
attrs.set_time(atime, mtime);
}),
&[
Token::Tuple { len: 1 },
Token::U32(
SSH_FILEXFER_ATTR_SIZE
| SSH_FILEXFER_ATTR_UIDGID
| SSH_FILEXFER_ATTR_PERMISSIONS
| SSH_FILEXFER_ATTR_ACMODTIME,
),
Token::U64(2333), Token::U32(u32::MAX), Token::U32(1000), Token::U32(Permissions::READ_BY_OWNER.bits()), Token::U32(atime.into_raw()), Token::U32(mtime.into_raw()), Token::TupleEnd,
],
);
}
}