2023-02-25 14:30:45 +00:00
|
|
|
pub mod device;
|
|
|
|
pub mod manager;
|
|
|
|
pub mod offer;
|
|
|
|
pub mod source;
|
|
|
|
|
2023-04-29 22:08:02 +01:00
|
|
|
use self::device::{DataControlDeviceDataExt, DataControlDeviceHandler};
|
2025-02-06 17:29:33 +01:00
|
|
|
use self::offer::{DataControlDeviceOffer, DataControlOfferHandler};
|
2023-04-29 22:08:02 +01:00
|
|
|
use self::source::DataControlSourceHandler;
|
2024-01-07 23:50:10 +00:00
|
|
|
use super::{Client, Environment, Event, Request, Response};
|
2025-02-21 16:35:54 +00:00
|
|
|
use crate::{Ironbar, lock, spawn, try_send};
|
2025-01-16 23:01:48 +00:00
|
|
|
use color_eyre::Result;
|
2023-04-29 22:08:02 +01:00
|
|
|
use device::DataControlDevice;
|
2023-02-25 14:30:45 +00:00
|
|
|
use glib::Bytes;
|
2025-01-16 23:33:02 +00:00
|
|
|
use rustix::buffer::spare_capacity;
|
2025-01-16 23:01:48 +00:00
|
|
|
use rustix::event::epoll;
|
|
|
|
use rustix::event::epoll::CreateFlags;
|
2025-01-16 23:33:02 +00:00
|
|
|
use rustix::fs::Timespec;
|
2025-01-16 23:01:48 +00:00
|
|
|
use rustix::pipe::{fcntl_getpipe_size, fcntl_setpipe_size};
|
2023-04-29 22:08:02 +01:00
|
|
|
use smithay_client_toolkit::data_device_manager::WritePipe;
|
2023-04-30 22:50:43 +01:00
|
|
|
use std::cmp::min;
|
2023-04-29 22:08:02 +01:00
|
|
|
use std::fmt::{Debug, Formatter};
|
2023-02-25 14:30:45 +00:00
|
|
|
use std::fs::File;
|
2025-02-06 17:29:33 +01:00
|
|
|
use std::io::{ErrorKind, Write};
|
2025-01-16 23:01:48 +00:00
|
|
|
use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
|
2023-02-25 14:30:45 +00:00
|
|
|
use std::sync::Arc;
|
2025-01-16 23:33:02 +00:00
|
|
|
use std::time::Duration;
|
2023-04-30 22:50:43 +01:00
|
|
|
use std::{fs, io};
|
2025-02-06 17:29:33 +01:00
|
|
|
use tokio::io::AsyncReadExt;
|
2024-01-07 23:50:10 +00:00
|
|
|
use tokio::sync::broadcast;
|
2023-04-30 22:50:43 +01:00
|
|
|
use tracing::{debug, error, trace};
|
2023-04-29 22:08:02 +01:00
|
|
|
use wayland_client::{Connection, QueueHandle};
|
|
|
|
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1;
|
2023-02-25 14:30:45 +00:00
|
|
|
|
|
|
|
const INTERNAL_MIME_TYPE: &str = "x-ironbar-internal";
|
|
|
|
|
2024-01-07 23:50:10 +00:00
|
|
|
/// Represents a value which can be read/written
|
|
|
|
/// to/from the system clipboard and surrounding metadata.
|
|
|
|
///
|
|
|
|
/// Can be cheaply cloned.
|
2023-02-25 14:30:45 +00:00
|
|
|
#[derive(Debug, Clone, Eq)]
|
|
|
|
pub struct ClipboardItem {
|
|
|
|
pub id: usize,
|
2024-01-07 23:50:10 +00:00
|
|
|
pub value: Arc<ClipboardValue>,
|
|
|
|
pub mime_type: Arc<str>,
|
2023-02-25 14:30:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl PartialEq<Self> for ClipboardItem {
|
|
|
|
fn eq(&self, other: &Self) -> bool {
|
|
|
|
self.id == other.id
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-29 22:08:02 +01:00
|
|
|
#[derive(Clone, PartialEq, Eq)]
|
2023-02-25 14:30:45 +00:00
|
|
|
pub enum ClipboardValue {
|
|
|
|
Text(String),
|
|
|
|
Image(Bytes),
|
|
|
|
Other,
|
|
|
|
}
|
|
|
|
|
2023-04-29 22:08:02 +01:00
|
|
|
impl Debug for ClipboardValue {
|
|
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
|
|
write!(
|
|
|
|
f,
|
|
|
|
"{}",
|
|
|
|
match self {
|
|
|
|
Self::Text(text) => text.clone(),
|
|
|
|
Self::Image(bytes) => {
|
|
|
|
format!("[{} Bytes]", bytes.len())
|
|
|
|
}
|
|
|
|
Self::Other => "[Unknown]".to_string(),
|
2023-02-25 14:30:45 +00:00
|
|
|
}
|
2023-04-29 22:08:02 +01:00
|
|
|
)
|
|
|
|
}
|
2023-02-25 14:30:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
struct MimeType {
|
|
|
|
value: String,
|
|
|
|
category: MimeTypeCategory,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
enum MimeTypeCategory {
|
|
|
|
Text,
|
|
|
|
Image,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl MimeType {
|
2023-04-29 22:08:02 +01:00
|
|
|
fn parse(mime_type: &str) -> Option<Self> {
|
|
|
|
match mime_type.to_lowercase().as_str() {
|
|
|
|
"text"
|
|
|
|
| "string"
|
|
|
|
| "utf8_string"
|
|
|
|
| "text/plain"
|
|
|
|
| "text/plain;charset=utf-8"
|
|
|
|
| "text/plain;charset=iso-8859-1"
|
|
|
|
| "text/plain;charset=us-ascii"
|
|
|
|
| "text/plain;charset=unicode" => Some(Self {
|
|
|
|
value: mime_type.to_string(),
|
|
|
|
category: MimeTypeCategory::Text,
|
|
|
|
}),
|
|
|
|
"image/png" | "image/jpg" | "image/jpeg" | "image/tiff" | "image/bmp"
|
|
|
|
| "image/x-bmp" | "image/icon" => Some(Self {
|
|
|
|
value: mime_type.to_string(),
|
|
|
|
category: MimeTypeCategory::Image,
|
|
|
|
}),
|
|
|
|
_ => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_multiple(mime_types: &[String]) -> Option<Self> {
|
|
|
|
mime_types.iter().find_map(|mime| Self::parse(mime))
|
2023-02-25 14:30:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-07 23:50:10 +00:00
|
|
|
impl Client {
|
|
|
|
/// Gets the current clipboard item,
|
|
|
|
/// if this exists and Ironbar has record of it.
|
|
|
|
pub fn clipboard_item(&self) -> Option<ClipboardItem> {
|
|
|
|
match self.send_request(Request::ClipboardItem) {
|
|
|
|
Response::ClipboardItem(item) => item,
|
|
|
|
_ => unreachable!(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Copies the provided value to the system clipboard.
|
|
|
|
pub fn copy_to_clipboard(&self, item: ClipboardItem) {
|
|
|
|
match self.send_request(Request::CopyToClipboard(item)) {
|
|
|
|
Response::Ok => (),
|
|
|
|
_ => unreachable!(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Subscribes to the system clipboard,
|
|
|
|
/// receiving all new copied items.
|
|
|
|
pub fn subscribe_clipboard(&self) -> broadcast::Receiver<ClipboardItem> {
|
|
|
|
self.clipboard_channel.0.subscribe()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-29 22:08:02 +01:00
|
|
|
impl Environment {
|
2024-01-07 23:50:10 +00:00
|
|
|
/// Creates a new copy/paste source on the
|
|
|
|
/// seat's data control device.
|
|
|
|
///
|
|
|
|
/// This provides it as an offer,
|
|
|
|
/// which the compositor will then treat as the current copied value.
|
|
|
|
pub fn copy_to_clipboard(&mut self, item: ClipboardItem) {
|
2023-04-30 22:50:43 +01:00
|
|
|
debug!("Copying item to clipboard: {item:?}");
|
2023-04-29 22:08:02 +01:00
|
|
|
|
2024-01-07 23:50:10 +00:00
|
|
|
let seat = self.default_seat();
|
|
|
|
let Some(device) = self
|
|
|
|
.data_control_devices
|
|
|
|
.iter()
|
|
|
|
.find(|entry| entry.seat == seat)
|
|
|
|
else {
|
|
|
|
return;
|
|
|
|
};
|
2023-02-25 14:30:45 +00:00
|
|
|
|
2024-01-07 23:50:10 +00:00
|
|
|
let source = self
|
|
|
|
.data_control_device_manager_state
|
2025-01-16 23:33:02 +00:00
|
|
|
.create_copy_paste_source(&self.queue_handle, [&item.mime_type, INTERNAL_MIME_TYPE]);
|
2023-02-25 14:30:45 +00:00
|
|
|
|
2024-01-07 23:50:10 +00:00
|
|
|
source.set_selection(&device.device);
|
|
|
|
self.copy_paste_sources.push(source);
|
|
|
|
|
|
|
|
lock!(self.clipboard).replace(item);
|
2023-04-29 22:08:02 +01:00
|
|
|
}
|
|
|
|
|
2024-01-07 23:50:10 +00:00
|
|
|
/// Reads an offer file handle into a new `ClipboardItem`.
|
2025-02-06 17:29:33 +01:00
|
|
|
async fn read_file(
|
|
|
|
mime_type: &MimeType,
|
|
|
|
file: &mut tokio::net::unix::pipe::Receiver,
|
|
|
|
) -> io::Result<ClipboardItem> {
|
|
|
|
let mut buf = vec![];
|
|
|
|
file.read_to_end(&mut buf).await?;
|
|
|
|
|
2023-04-29 22:08:02 +01:00
|
|
|
let value = match mime_type.category {
|
|
|
|
MimeTypeCategory::Text => {
|
2025-02-06 17:29:33 +01:00
|
|
|
let txt = String::from_utf8_lossy(&buf).to_string();
|
2023-04-29 22:08:02 +01:00
|
|
|
ClipboardValue::Text(txt)
|
|
|
|
}
|
|
|
|
MimeTypeCategory::Image => {
|
2025-02-06 17:29:33 +01:00
|
|
|
let bytes = Bytes::from(&buf);
|
2023-04-29 22:08:02 +01:00
|
|
|
ClipboardValue::Image(bytes)
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(ClipboardItem {
|
2023-12-08 22:39:27 +00:00
|
|
|
id: Ironbar::unique_id(),
|
2024-01-07 23:50:10 +00:00
|
|
|
value: Arc::new(value),
|
|
|
|
mime_type: mime_type.value.clone().into(),
|
2023-04-29 22:08:02 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl DataControlDeviceHandler for Environment {
|
2024-01-07 23:50:10 +00:00
|
|
|
/// Called when an offer for a new value is received
|
|
|
|
/// (ie something has copied to the clipboard)
|
2023-04-29 22:08:02 +01:00
|
|
|
fn selection(
|
|
|
|
&mut self,
|
|
|
|
_conn: &Connection,
|
|
|
|
_qh: &QueueHandle<Self>,
|
|
|
|
data_device: DataControlDevice,
|
|
|
|
) {
|
|
|
|
debug!("Handler received selection event");
|
|
|
|
|
|
|
|
let mime_types = data_device.selection_mime_types();
|
2023-02-25 14:30:45 +00:00
|
|
|
|
|
|
|
if mime_types.contains(&INTERNAL_MIME_TYPE.to_string()) {
|
2023-04-29 22:08:02 +01:00
|
|
|
return;
|
2023-02-25 14:30:45 +00:00
|
|
|
}
|
|
|
|
|
2023-04-29 22:08:02 +01:00
|
|
|
if let Some(offer) = data_device.selection_offer() {
|
2024-01-07 23:50:10 +00:00
|
|
|
// clear prev
|
2023-04-29 22:08:02 +01:00
|
|
|
let Some(mime_type) = MimeType::parse_multiple(&mime_types) else {
|
|
|
|
lock!(self.clipboard).take();
|
2023-02-25 14:30:45 +00:00
|
|
|
// send an event so the clipboard module is aware it's changed
|
2024-01-07 23:50:10 +00:00
|
|
|
try_send!(
|
|
|
|
self.event_tx,
|
|
|
|
Event::Clipboard(ClipboardItem {
|
2023-02-25 14:30:45 +00:00
|
|
|
id: usize::MAX,
|
2024-01-07 23:50:10 +00:00
|
|
|
mime_type: String::new().into(),
|
|
|
|
value: Arc::new(ClipboardValue::Other)
|
2023-02-25 14:30:45 +00:00
|
|
|
})
|
|
|
|
);
|
2023-04-29 22:08:02 +01:00
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
2024-02-18 00:41:16 +00:00
|
|
|
debug!("Receiving mime type: {}", mime_type.value);
|
2025-02-06 17:29:33 +01:00
|
|
|
if let Ok(mut read_pipe) = offer.receive(mime_type.value.clone()) {
|
2024-01-07 23:50:10 +00:00
|
|
|
let tx = self.event_tx.clone();
|
2023-04-29 22:08:02 +01:00
|
|
|
let clipboard = self.clipboard.clone();
|
|
|
|
|
2025-02-06 17:29:33 +01:00
|
|
|
spawn(async move {
|
|
|
|
match Self::read_file(&mime_type, &mut read_pipe).await {
|
|
|
|
Ok(item) => {
|
|
|
|
lock!(clipboard).replace(item.clone());
|
|
|
|
try_send!(tx, Event::Clipboard(item));
|
|
|
|
}
|
|
|
|
Err(err) => error!("{err:?}"),
|
2023-04-29 22:08:02 +01:00
|
|
|
}
|
2025-02-06 17:29:33 +01:00
|
|
|
});
|
2023-02-25 14:30:45 +00:00
|
|
|
}
|
|
|
|
}
|
2023-04-29 22:08:02 +01:00
|
|
|
}
|
|
|
|
}
|
2023-02-25 14:30:45 +00:00
|
|
|
|
2023-04-29 22:08:02 +01:00
|
|
|
impl DataControlOfferHandler for Environment {
|
|
|
|
fn offer(
|
|
|
|
&mut self,
|
|
|
|
_conn: &Connection,
|
|
|
|
_qh: &QueueHandle<Self>,
|
|
|
|
_offer: &mut DataControlDeviceOffer,
|
|
|
|
_mime_type: String,
|
|
|
|
) {
|
2023-08-16 20:27:24 +01:00
|
|
|
trace!("Handler received offer");
|
2023-02-25 14:30:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-29 22:08:02 +01:00
|
|
|
impl DataControlSourceHandler for Environment {
|
2024-05-11 20:55:43 +01:00
|
|
|
// fn accept_mime(
|
|
|
|
// &mut self,
|
|
|
|
// _conn: &Connection,
|
|
|
|
// _qh: &QueueHandle<Self>,
|
|
|
|
// _source: &ZwlrDataControlSourceV1,
|
|
|
|
// mime: Option<String>,
|
|
|
|
// ) {
|
|
|
|
// debug!("Accepted mime type: {mime:?}");
|
|
|
|
// }
|
2023-02-25 14:30:45 +00:00
|
|
|
|
2024-01-07 23:50:10 +00:00
|
|
|
/// Writes the current clipboard item to 'paste' it
|
|
|
|
/// upon request from a compositor client.
|
2023-04-29 22:08:02 +01:00
|
|
|
fn send_request(
|
|
|
|
&mut self,
|
|
|
|
_conn: &Connection,
|
|
|
|
_qh: &QueueHandle<Self>,
|
|
|
|
source: &ZwlrDataControlSourceV1,
|
|
|
|
mime: String,
|
|
|
|
write_pipe: WritePipe,
|
2025-01-16 23:01:48 +00:00
|
|
|
) -> Result<()> {
|
2023-04-30 22:50:43 +01:00
|
|
|
debug!("Handler received source send request event ({mime})");
|
2023-04-29 22:08:02 +01:00
|
|
|
|
|
|
|
if let Some(item) = lock!(self.clipboard).clone() {
|
|
|
|
let fd = OwnedFd::from(write_pipe);
|
2023-04-30 22:50:43 +01:00
|
|
|
if self
|
2023-04-29 22:08:02 +01:00
|
|
|
.copy_paste_sources
|
|
|
|
.iter_mut()
|
2023-04-30 22:50:43 +01:00
|
|
|
.any(|s| s.inner() == source && MimeType::parse(&mime).is_some())
|
2023-04-29 22:08:02 +01:00
|
|
|
{
|
2023-04-30 22:50:43 +01:00
|
|
|
trace!("Source found, writing to file");
|
2023-04-29 22:08:02 +01:00
|
|
|
|
2024-01-07 23:50:10 +00:00
|
|
|
let mut bytes = match item.value.as_ref() {
|
2023-04-29 22:08:02 +01:00
|
|
|
ClipboardValue::Text(text) => text.as_bytes(),
|
|
|
|
ClipboardValue::Image(bytes) => bytes.as_ref(),
|
|
|
|
ClipboardValue::Other => panic!(
|
|
|
|
"{:?}",
|
2024-01-07 23:50:10 +00:00
|
|
|
io::Error::new(ErrorKind::Other, "Attempted to copy unsupported mime type")
|
2023-04-29 22:08:02 +01:00
|
|
|
),
|
|
|
|
};
|
|
|
|
|
2025-01-16 23:01:48 +00:00
|
|
|
let pipe_size =
|
|
|
|
set_pipe_size(fd.as_fd(), bytes.len()).expect("Failed to increase pipe size");
|
2024-02-18 00:41:16 +00:00
|
|
|
let mut file = File::from(fd.try_clone().expect("to be able to clone"));
|
2023-04-30 22:50:43 +01:00
|
|
|
|
2024-02-18 00:41:16 +00:00
|
|
|
debug!("Writing {} bytes", bytes.len());
|
2023-04-30 22:50:43 +01:00
|
|
|
|
2025-01-16 23:01:48 +00:00
|
|
|
let epoll = epoll::create(CreateFlags::CLOEXEC)?;
|
|
|
|
epoll::add(
|
|
|
|
&epoll,
|
|
|
|
fd,
|
|
|
|
epoll::EventData::new_u64(0),
|
|
|
|
epoll::EventFlags::OUT,
|
|
|
|
)?;
|
2023-04-30 22:50:43 +01:00
|
|
|
|
2025-01-16 23:33:02 +00:00
|
|
|
let mut events = Vec::with_capacity(16);
|
2023-04-30 22:50:43 +01:00
|
|
|
|
|
|
|
while !bytes.is_empty() {
|
2025-01-16 23:01:48 +00:00
|
|
|
let chunk = &bytes[..min(pipe_size, bytes.len())];
|
2023-04-30 22:50:43 +01:00
|
|
|
|
2025-01-16 23:33:02 +00:00
|
|
|
epoll::wait(
|
|
|
|
&epoll,
|
|
|
|
spare_capacity(&mut events),
|
|
|
|
Some(&Timespec::try_from(Duration::from_millis(100))?),
|
|
|
|
)?;
|
2023-04-30 22:50:43 +01:00
|
|
|
|
|
|
|
match file.write(chunk) {
|
2024-02-18 00:41:16 +00:00
|
|
|
Ok(written) => {
|
|
|
|
trace!("Wrote {} bytes ({} remain)", written, bytes.len());
|
|
|
|
bytes = &bytes[written..];
|
|
|
|
}
|
2023-04-30 22:50:43 +01:00
|
|
|
Err(err) => {
|
|
|
|
error!("{err:?}");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2023-04-29 22:08:02 +01:00
|
|
|
}
|
2024-02-18 00:41:16 +00:00
|
|
|
|
|
|
|
debug!("Done writing");
|
2023-04-30 22:50:43 +01:00
|
|
|
} else {
|
2025-01-16 23:33:02 +00:00
|
|
|
error!("Failed to find source (mime: '{mime}')");
|
2023-04-29 22:08:02 +01:00
|
|
|
}
|
2023-02-25 14:30:45 +00:00
|
|
|
}
|
2025-01-16 23:01:48 +00:00
|
|
|
|
|
|
|
Ok(())
|
2023-04-29 22:08:02 +01:00
|
|
|
}
|
2023-02-25 14:30:45 +00:00
|
|
|
|
2023-04-29 22:08:02 +01:00
|
|
|
fn cancelled(
|
|
|
|
&mut self,
|
|
|
|
_conn: &Connection,
|
|
|
|
_qh: &QueueHandle<Self>,
|
|
|
|
source: &ZwlrDataControlSourceV1,
|
|
|
|
) {
|
|
|
|
debug!("Handler received source cancelled event");
|
2023-02-25 14:30:45 +00:00
|
|
|
|
2023-04-29 22:08:02 +01:00
|
|
|
self.copy_paste_sources
|
|
|
|
.iter()
|
|
|
|
.position(|s| s.inner() == source)
|
|
|
|
.map(|pos| self.copy_paste_sources.remove(pos));
|
|
|
|
source.destroy();
|
|
|
|
}
|
2023-02-25 14:30:45 +00:00
|
|
|
}
|
2023-04-30 22:50:43 +01:00
|
|
|
|
|
|
|
/// Attempts to increase the fd pipe size to the requested number of bytes.
|
|
|
|
/// The kernel will automatically round this up to the nearest page size.
|
|
|
|
/// If the requested size is larger than the kernel max (normally 1MB),
|
|
|
|
/// it will be clamped at this.
|
|
|
|
///
|
2024-02-18 00:41:16 +00:00
|
|
|
/// Returns the new size if succeeded.
|
2025-01-16 23:01:48 +00:00
|
|
|
fn set_pipe_size(fd: BorrowedFd, size: usize) -> io::Result<usize> {
|
2023-04-30 22:50:43 +01:00
|
|
|
// clamp size at kernel max
|
|
|
|
let max_pipe_size = fs::read_to_string("/proc/sys/fs/pipe-max-size")
|
|
|
|
.expect("Failed to find pipe-max-size virtual kernel file")
|
|
|
|
.trim()
|
|
|
|
.parse::<usize>()
|
|
|
|
.expect("Failed to parse pipe-max-size contents");
|
|
|
|
|
|
|
|
let size = min(size, max_pipe_size);
|
|
|
|
|
2025-01-16 23:01:48 +00:00
|
|
|
let curr_size = fcntl_getpipe_size(fd)?;
|
2023-04-30 22:50:43 +01:00
|
|
|
|
|
|
|
trace!("Current pipe size: {curr_size}");
|
|
|
|
|
|
|
|
let new_size = if size > curr_size {
|
|
|
|
trace!("Requesting pipe size increase to (at least): {size}");
|
2023-07-16 20:09:22 +01:00
|
|
|
|
2025-01-16 23:01:48 +00:00
|
|
|
fcntl_setpipe_size(fd, size)?;
|
|
|
|
let res = fcntl_getpipe_size(fd)?;
|
2023-04-30 22:50:43 +01:00
|
|
|
trace!("New pipe size: {res}");
|
2023-07-16 20:09:22 +01:00
|
|
|
|
2025-01-16 23:01:48 +00:00
|
|
|
if res < size {
|
2023-04-30 22:50:43 +01:00
|
|
|
return Err(io::Error::last_os_error());
|
|
|
|
}
|
2023-07-16 20:09:22 +01:00
|
|
|
|
2023-04-30 22:50:43 +01:00
|
|
|
res
|
|
|
|
} else {
|
2025-01-16 23:01:48 +00:00
|
|
|
size
|
2023-04-30 22:50:43 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
Ok(new_size)
|
|
|
|
}
|