use std::fs::{File, OpenOptions};
use std::io::{Seek, SeekFrom};
use std::path::{Path, PathBuf};
use crate::metadata::{Metadata, MetadataLimits};
#[derive(Debug)]
pub struct AudioInputSession {
path: PathBuf,
file: File,
len: u64,
}
impl AudioInputSession {
pub fn open(path: impl AsRef<Path>) -> Result<Self, String> {
let path = path.as_ref();
let (file, len) = open_regular_file(path, "audio input")?;
Ok(Self {
path: path.to_path_buf(),
file,
len,
})
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
#[must_use]
pub fn len(&self) -> u64 {
self.len
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn read_metadata(&mut self) -> Result<Option<Metadata>, String> {
self.read_metadata_with_limits(MetadataLimits::default())
}
pub fn read_metadata_with_limits(
&mut self,
limits: MetadataLimits,
) -> Result<Option<Metadata>, String> {
crate::metadata::read_extended_from_file_with_limits(&mut self.file, &self.path, limits)
}
pub(crate) fn try_clone_rewound(&mut self, context: &str) -> Result<File, String> {
self.file
.seek(SeekFrom::Start(0))
.map_err(|error| format!("rewind {context} {}: {error}", self.path.display()))?;
let mut clone = self
.file
.try_clone()
.map_err(|error| format!("clone {context} {}: {error}", self.path.display()))?;
clone
.seek(SeekFrom::Start(0))
.map_err(|error| format!("rewind cloned {context} {}: {error}", self.path.display()))?;
Ok(clone)
}
pub(crate) fn into_file_rewound(mut self, context: &str) -> Result<File, String> {
self.file
.seek(SeekFrom::Start(0))
.map_err(|error| format!("rewind {context} {}: {error}", self.path.display()))?;
Ok(self.file)
}
}
pub(crate) fn open_regular_file(path: &Path, context: &str) -> Result<(File, u64), String> {
let mut options = OpenOptions::new();
options.read(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt as _;
options.custom_flags(libc::O_NONBLOCK | libc::O_NOCTTY);
}
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt as _;
use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS;
options.custom_flags(FILE_FLAG_BACKUP_SEMANTICS);
}
let file = options
.open(path)
.map_err(|error| format!("open {context} {}: {error}", path.display()))?;
let metadata = file
.metadata()
.map_err(|error| format!("inspect {context} {}: {error}", path.display()))?;
#[cfg(windows)]
ensure_windows_disk_handle(&file, path, context)?;
if !metadata.is_file() {
return Err(format!(
"{context} is not a regular file: {}",
path.display()
));
}
#[cfg(unix)]
clear_unix_nonblocking(&file, path, context)?;
Ok((file, metadata.len()))
}
#[cfg(unix)]
fn clear_unix_nonblocking(file: &File, path: &Path, context: &str) -> Result<(), String> {
use std::os::fd::AsRawFd as _;
let descriptor = file.as_raw_fd();
let flags = unsafe { libc::fcntl(descriptor, libc::F_GETFL) };
if flags == -1 {
return Err(format!(
"inspect {context} flags {}: {}",
path.display(),
std::io::Error::last_os_error()
));
}
if flags & libc::O_NONBLOCK != 0 {
if unsafe { libc::fcntl(descriptor, libc::F_SETFL, flags & !libc::O_NONBLOCK) } == -1 {
return Err(format!(
"set blocking {context} mode {}: {}",
path.display(),
std::io::Error::last_os_error()
));
}
}
Ok(())
}
#[cfg(windows)]
fn ensure_windows_disk_handle(file: &File, path: &Path, context: &str) -> Result<(), String> {
use std::os::windows::io::AsRawHandle as _;
use windows_sys::Win32::Storage::FileSystem::{GetFileType, FILE_TYPE_DISK};
let file_type = unsafe { GetFileType(file.as_raw_handle()) };
if file_type != FILE_TYPE_DISK {
return Err(format!("{context} is not a disk file: {}", path.display()));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
use std::fs;
use std::io::Write as _;
#[test]
fn regular_file_reports_open_handle_length() {
let mut input = tempfile::NamedTempFile::new().unwrap();
input.write_all(b"regular audio bytes").unwrap();
input.flush().unwrap();
let session = AudioInputSession::open(input.path()).unwrap();
assert_eq!(session.path(), input.path());
assert_eq!(session.len(), 19);
assert!(!session.is_empty());
#[cfg(unix)]
{
use std::os::fd::AsRawFd as _;
let flags = unsafe { libc::fcntl(session.file.as_raw_fd(), libc::F_GETFL) };
assert_ne!(flags, -1);
assert_eq!(flags & libc::O_NONBLOCK, 0);
}
}
#[cfg(unix)]
#[test]
fn symlink_to_regular_file_is_accepted() {
use std::os::unix::fs::symlink;
let directory = tempfile::tempdir().unwrap();
let target = directory.path().join("target.audio");
let link = directory.path().join("input.audio");
fs::write(&target, b"linked bytes").unwrap();
symlink(&target, &link).unwrap();
let session = AudioInputSession::open(&link).unwrap();
assert_eq!(session.path(), link);
assert_eq!(session.len(), 12);
}
#[cfg(unix)]
#[test]
fn fifo_is_rejected_without_waiting_for_a_writer() {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt as _;
use std::os::unix::fs::FileTypeExt as _;
let directory = tempfile::tempdir().unwrap();
let fifo = directory.path().join("input.fifo");
let fifo_name = CString::new(fifo.as_os_str().as_bytes()).unwrap();
assert_eq!(unsafe { libc::mkfifo(fifo_name.as_ptr(), 0o600) }, 0);
let error = AudioInputSession::open(&fifo).unwrap_err();
assert!(error.contains("not a regular file"), "{error}");
assert!(fs::symlink_metadata(&fifo).unwrap().file_type().is_fifo());
}
#[cfg(unix)]
#[test]
fn device_is_rejected_before_reading() {
let error = AudioInputSession::open("/dev/null").unwrap_err();
assert!(error.contains("not a regular file"), "{error}");
}
#[test]
fn directory_is_rejected_before_reading() {
let directory = tempfile::tempdir().unwrap();
let error = AudioInputSession::open(directory.path()).unwrap_err();
assert!(error.contains("not a regular file"), "{error}");
}
}