use super::state::VirtualDeviceState;
use crate::ptp::{
pack_string, pack_u16, pack_u16_array, pack_u32, pack_u64, DateTime, EventCode,
ObjectFormatCode, ObjectHandle, OperationCode, StorageId, MAX_STRING_UNITS,
};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub(super) fn pack_device_string(s: &str) -> Vec<u8> {
let mut units = 0;
let end = s
.char_indices()
.find(|&(_, c)| {
units += c.len_utf16();
units > MAX_STRING_UNITS
})
.map_or(s.len(), |(i, _)| i);
pack_string(&s[..end]).expect("truncated to MAX_STRING_UNITS, so it always fits")
}
pub(super) fn build_device_info(state: &VirtualDeviceState) -> Vec<u8> {
let mut buf = Vec::with_capacity(512);
buf.extend_from_slice(&pack_u16(100));
buf.extend_from_slice(&pack_u32(6));
buf.extend_from_slice(&pack_u16(100));
buf.extend_from_slice(&pack_device_string("microsoft.com: 1.0"));
buf.extend_from_slice(&pack_u16(0));
let mut ops: Vec<u16> = vec![
OperationCode::GetDeviceInfo.into(),
OperationCode::OpenSession.into(),
OperationCode::CloseSession.into(),
OperationCode::GetStorageIds.into(),
OperationCode::GetStorageInfo.into(),
OperationCode::GetObjectHandles.into(),
OperationCode::GetObjectInfo.into(),
OperationCode::GetObject.into(),
OperationCode::GetThumb.into(),
OperationCode::SendObjectInfo.into(),
OperationCode::SendObject.into(),
OperationCode::DeleteObject.into(),
OperationCode::MoveObject.into(),
OperationCode::CopyObject.into(),
OperationCode::GetObjectPropValue.into(),
];
if state.config.supports_partial_object {
ops.push(OperationCode::GetPartialObject.into());
}
if state.config.supports_partial_object_64 {
ops.push(OperationCode::GetPartialObject64.into());
}
if state.config.supports_rename {
ops.push(OperationCode::SetObjectPropValue.into());
}
if state.config.supports_object_prop_list {
ops.push(OperationCode::GetObjectPropList.into());
}
buf.extend_from_slice(&pack_u16_array(&ops));
let events: Vec<u16> = vec![
EventCode::ObjectAdded.into(),
EventCode::ObjectRemoved.into(),
EventCode::StorageInfoChanged.into(),
];
buf.extend_from_slice(&pack_u16_array(&events));
buf.extend_from_slice(&pack_u16_array(&[]));
buf.extend_from_slice(&pack_u16_array(&[]));
let playback: Vec<u16> = vec![
ObjectFormatCode::Undefined.into(),
ObjectFormatCode::Association.into(),
ObjectFormatCode::Text.into(),
ObjectFormatCode::Jpeg.into(),
ObjectFormatCode::Png.into(),
ObjectFormatCode::Mp3.into(),
ObjectFormatCode::Mp4Container.into(),
];
buf.extend_from_slice(&pack_u16_array(&playback));
buf.extend_from_slice(&pack_device_string(&state.config.manufacturer));
buf.extend_from_slice(&pack_device_string(&state.config.model));
buf.extend_from_slice(&pack_device_string("1.0.0"));
buf.extend_from_slice(&pack_device_string(&state.config.serial));
buf
}
pub(super) fn build_storage_info(
state: &VirtualDeviceState,
storage_id: StorageId,
) -> Option<Vec<u8>> {
let storage = state.find_storage(storage_id)?;
let mut buf = Vec::with_capacity(128);
buf.extend_from_slice(&pack_u16(3));
buf.extend_from_slice(&pack_u16(2));
let access = if storage.config.read_only { 1u16 } else { 0u16 };
buf.extend_from_slice(&pack_u16(access));
buf.extend_from_slice(&pack_u64(storage.config.capacity));
let free = compute_free_space(&storage.config);
buf.extend_from_slice(&pack_u64(free));
buf.extend_from_slice(&pack_u32(0xFFFF_FFFF));
buf.extend_from_slice(&pack_device_string(&storage.config.description));
buf.extend_from_slice(&pack_device_string(""));
Some(buf)
}
fn compute_free_space(config: &super::config::VirtualStorageConfig) -> u64 {
let used = dir_size(&config.backing_dir);
config.capacity.saturating_sub(used)
}
fn dir_size(path: &std::path::Path) -> u64 {
if !path.is_dir() {
return 0;
}
let mut total = 0u64;
if let Ok(entries) = std::fs::read_dir(path) {
for entry in entries.flatten() {
let ft = match entry.file_type() {
Ok(ft) => ft,
Err(_) => continue,
};
if ft.is_file() {
total += entry.metadata().map(|m| m.len()).unwrap_or(0);
} else if ft.is_dir() {
total += dir_size(&entry.path());
}
}
}
total
}
pub(super) fn build_response(code: u16, tx_id: u32, params: &[u32]) -> Vec<u8> {
let len = 12 + params.len() * 4;
let mut buf = Vec::with_capacity(len);
buf.extend_from_slice(&pack_u32(len as u32));
buf.extend_from_slice(&pack_u16(3)); buf.extend_from_slice(&pack_u16(code));
buf.extend_from_slice(&pack_u32(tx_id));
for &p in params {
buf.extend_from_slice(&pack_u32(p));
}
buf
}
pub(super) fn build_data_container(op_code: u16, tx_id: u32, payload: &[u8]) -> Vec<u8> {
let len = 12 + payload.len();
let mut buf = Vec::with_capacity(len);
buf.extend_from_slice(&pack_u32(len as u32));
buf.extend_from_slice(&pack_u16(2)); buf.extend_from_slice(&pack_u16(op_code));
buf.extend_from_slice(&pack_u32(tx_id));
buf.extend_from_slice(payload);
buf
}
pub(super) fn build_event(code: EventCode, params: &[u32]) -> Vec<u8> {
let param_count = params.len().min(3);
let len = 12 + param_count * 4;
let mut buf = Vec::with_capacity(len);
buf.extend_from_slice(&pack_u32(len as u32));
buf.extend_from_slice(&pack_u16(4)); buf.extend_from_slice(&pack_u16(code.into()));
buf.extend_from_slice(&pack_u32(0)); for &p in params.iter().take(param_count) {
buf.extend_from_slice(&pack_u32(p));
}
buf
}
pub(super) fn pack_file_time(time: Option<SystemTime>, state: &VirtualDeviceState) -> Vec<u8> {
let config = &state.config;
let formatted = time
.and_then(unix_seconds)
.and_then(|secs| DateTime::from_unix_seconds(secs, config.utc_offset))
.map(|dt| DateTime {
offset: dt.offset.filter(|_| config.dates_include_offset),
..dt
})
.and_then(|dt| dt.format())
.unwrap_or_default();
pack_device_string(&formatted)
}
fn unix_seconds(time: SystemTime) -> Option<i64> {
match time.duration_since(UNIX_EPOCH) {
Ok(after) => i64::try_from(after.as_secs()).ok(),
Err(before) => {
let before = before.duration();
let secs = i64::try_from(before.as_secs()).ok()?;
Some(if before.subsec_nanos() > 0 {
-secs - 1
} else {
-secs
})
}
}
}
pub(super) fn system_time_from_unix(secs: i64) -> SystemTime {
let magnitude = Duration::from_secs(secs.unsigned_abs());
if secs >= 0 {
UNIX_EPOCH + magnitude
} else {
UNIX_EPOCH - magnitude
}
}
pub(super) struct ObjectFacts {
pub storage_id: StorageId,
pub parent: ObjectHandle,
pub filename: String,
pub is_dir: bool,
pub size: u64,
pub format: u16,
pub created: Option<SystemTime>,
pub modified: Option<SystemTime>,
}
pub(super) fn object_facts(
handle: ObjectHandle,
state: &VirtualDeviceState,
) -> Option<ObjectFacts> {
let obj = state.objects.get(&handle.0)?;
let storage = state.find_storage(obj.storage_id)?;
let metadata = std::fs::metadata(storage.config.backing_dir.join(&obj.rel_path)).ok()?;
let is_dir = metadata.is_dir();
let format = if is_dir {
ObjectFormatCode::Association
} else {
let ext = obj
.rel_path
.extension()
.unwrap_or_default()
.to_string_lossy();
ObjectFormatCode::from_extension(&ext)
};
Some(ObjectFacts {
storage_id: obj.storage_id,
parent: obj.parent,
filename: obj
.rel_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
is_dir,
size: if is_dir { 0 } else { metadata.len() },
format: format.into(),
created: metadata.created().ok(),
modified: metadata.modified().ok(),
})
}
pub(super) fn build_object_info(
handle: ObjectHandle,
state: &VirtualDeviceState,
) -> Option<Vec<u8>> {
let facts = object_facts(handle, state)?;
let size = facts.size.min(u64::from(u32::MAX)) as u32;
let mut buf = Vec::with_capacity(128);
buf.extend_from_slice(&pack_u32(facts.storage_id.0));
buf.extend_from_slice(&pack_u16(facts.format));
buf.extend_from_slice(&pack_u16(0));
buf.extend_from_slice(&pack_u32(size));
buf.extend_from_slice(&pack_u16(ObjectFormatCode::Undefined.into()));
buf.extend_from_slice(&pack_u32(0));
buf.extend_from_slice(&pack_u32(0));
buf.extend_from_slice(&pack_u32(0));
buf.extend_from_slice(&pack_u32(0));
buf.extend_from_slice(&pack_u32(0));
buf.extend_from_slice(&pack_u32(0));
buf.extend_from_slice(&pack_u32(facts.parent.0));
let assoc_type: u16 = if facts.is_dir { 1 } else { 0 };
buf.extend_from_slice(&pack_u16(assoc_type));
buf.extend_from_slice(&pack_u32(0));
buf.extend_from_slice(&pack_u32(0));
buf.extend_from_slice(&pack_device_string(&facts.filename));
buf.extend_from_slice(&pack_file_time(facts.created, state));
buf.extend_from_slice(&pack_file_time(facts.modified, state));
buf.push(0x00);
Some(buf)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ptp::unpack_string;
#[test]
fn device_strings_that_fit_go_out_unchanged() {
let s = "a".repeat(MAX_STRING_UNITS);
assert_eq!(pack_device_string(&s), pack_string(&s).unwrap());
}
#[test]
fn device_strings_too_long_for_ptp_are_cut_like_aosp_does() {
let (decoded, _) = unpack_string(&pack_device_string(&"a".repeat(300))).unwrap();
assert_eq!(decoded, "a".repeat(MAX_STRING_UNITS));
}
#[test]
fn device_string_cut_never_splits_a_surrogate_pair() {
let s = format!("{}\u{1F600}", "a".repeat(MAX_STRING_UNITS - 1));
let (decoded, _) = unpack_string(&pack_device_string(&s)).unwrap();
assert_eq!(decoded, "a".repeat(MAX_STRING_UNITS - 1));
}
}