Corrections mineurs sur pmoflac
This commit is contained in:
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -2734,11 +2734,14 @@ dependencies = [
|
||||
"lewton",
|
||||
"libc",
|
||||
"libflac-sys",
|
||||
"lofty",
|
||||
"minimp3",
|
||||
"opus",
|
||||
"pmometadata",
|
||||
"tempfile",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -13,13 +13,16 @@ path = "src/lib.rs"
|
||||
[dependencies]
|
||||
bytes = "1.6"
|
||||
claxon = "0.4"
|
||||
lewton = "0.10"
|
||||
libc = "0.2"
|
||||
libflac-sys = { version = "0.3.3", default-features = false, features = ["build-flac"] }
|
||||
lofty = "0.22"
|
||||
minimp3 = "0.5"
|
||||
opus = "0.3"
|
||||
pmometadata = { path = "../pmometadata" }
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.37", features = ["rt", "rt-multi-thread", "macros", "sync", "io-util", "fs"] }
|
||||
minimp3 = "0.5"
|
||||
lewton = "0.10"
|
||||
opus = "0.3"
|
||||
tracing = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.10"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
cmp, io,
|
||||
io,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
@@ -8,9 +8,9 @@ use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf};
|
||||
|
||||
use crate::{
|
||||
decode_aiff_stream, decode_flac_stream, decode_mp3_stream, decode_ogg_opus_stream,
|
||||
decode_ogg_vorbis_stream, decode_wav_stream, pcm::StreamInfo, AiffDecodedStream, AiffError,
|
||||
FlacDecodedStream, FlacError, Mp3DecodedStream, Mp3Error, OggDecodedStream, OggError,
|
||||
OggOpusDecodedStream, OggOpusError, WavDecodedStream, WavError,
|
||||
decode_ogg_vorbis_stream, decode_wav_stream, pcm::StreamInfo, prefixed_reader::PrefixedReader,
|
||||
AiffDecodedStream, AiffError, FlacDecodedStream, FlacError, Mp3DecodedStream, Mp3Error,
|
||||
OggDecodedStream, OggError, OggOpusDecodedStream, OggOpusError, WavDecodedStream, WavError,
|
||||
};
|
||||
|
||||
const MAX_SNIFF_BYTES: usize = 64 * 1024;
|
||||
@@ -290,39 +290,3 @@ enum DetectedFormat {
|
||||
Wav,
|
||||
Aiff,
|
||||
}
|
||||
|
||||
struct PrefixedReader<R> {
|
||||
prefix: Vec<u8>,
|
||||
position: usize,
|
||||
reader: R,
|
||||
}
|
||||
|
||||
impl<R> PrefixedReader<R> {
|
||||
fn new(prefix: Vec<u8>, reader: R) -> Self {
|
||||
Self {
|
||||
prefix,
|
||||
position: 0,
|
||||
reader,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: AsyncRead + Unpin> AsyncRead for PrefixedReader<R> {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
if self.position < self.prefix.len() && buf.remaining() > 0 {
|
||||
let remaining = self.prefix.len() - self.position;
|
||||
let to_copy = cmp::min(remaining, buf.remaining());
|
||||
buf.put_slice(&self.prefix[self.position..self.position + to_copy]);
|
||||
self.position += to_copy;
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
Pin::new(&mut self.reader).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Unpin> Unpin for PrefixedReader<R> {}
|
||||
unsafe impl<R: Send> Send for PrefixedReader<R> {}
|
||||
|
||||
@@ -102,11 +102,13 @@ pub mod decoder;
|
||||
mod decoder_common;
|
||||
pub mod encoder;
|
||||
pub mod error;
|
||||
pub mod metadata;
|
||||
pub mod mp3;
|
||||
pub mod ogg;
|
||||
mod ogg_common;
|
||||
pub mod opus;
|
||||
mod pcm;
|
||||
mod prefixed_reader;
|
||||
mod stream;
|
||||
pub mod transcode;
|
||||
mod util;
|
||||
@@ -117,6 +119,7 @@ pub use autodetect::{decode_audio_stream, DecodeAudioError, DecodedAudioStream,
|
||||
pub use decoder::{decode_flac_stream, FlacDecodedStream};
|
||||
pub use encoder::{encode_flac_stream, EncoderOptions, FlacEncodedStream};
|
||||
pub use error::FlacError;
|
||||
pub use metadata::AudioFileMetadata;
|
||||
pub use mp3::{decode_mp3_stream, Mp3DecodedStream, Mp3Error};
|
||||
pub use ogg::{decode_ogg_vorbis_stream, OggDecodedStream, OggError};
|
||||
pub use opus::{decode_ogg_opus_stream, OggOpusDecodedStream, OggOpusError};
|
||||
|
||||
297
pmoflac/src/metadata.rs
Normal file
297
pmoflac/src/metadata.rs
Normal file
@@ -0,0 +1,297 @@
|
||||
//! Audio file metadata extraction using lofty.
|
||||
//!
|
||||
//! This module provides utilities to extract both technical audio properties
|
||||
//! (sample rate, channels, bit depth) and artistic tags (title, artist, album)
|
||||
//! from audio files in various formats.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use pmoflac::AudioFileMetadata;
|
||||
//! use std::path::Path;
|
||||
//!
|
||||
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let metadata = AudioFileMetadata::from_file(Path::new("audio.flac"))?;
|
||||
//!
|
||||
//! println!("Title: {:?}", metadata.title);
|
||||
//! println!("Artist: {:?}", metadata.artist);
|
||||
//! println!("Sample rate: {:?} Hz", metadata.sample_rate);
|
||||
//! println!("Duration: {:?} seconds", metadata.duration_secs);
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use lofty::{config::ParseOptions, prelude::*, probe::Probe};
|
||||
use std::{io::Cursor, path::Path};
|
||||
|
||||
/// Comprehensive audio file metadata including both technical properties and artistic tags.
|
||||
///
|
||||
/// This struct is populated from audio file tags using the lofty library,
|
||||
/// which supports FLAC, MP3, Ogg Vorbis, Opus, WAV, AIFF, and other formats.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AudioFileMetadata {
|
||||
// Artistic tags
|
||||
/// Track title
|
||||
pub title: Option<String>,
|
||||
/// Artist name
|
||||
pub artist: Option<String>,
|
||||
/// Album name
|
||||
pub album: Option<String>,
|
||||
/// Year of release
|
||||
pub year: Option<u32>,
|
||||
/// Music genre
|
||||
pub genre: Option<String>,
|
||||
/// Track number in the album
|
||||
pub track_number: Option<u32>,
|
||||
/// Total number of tracks in the album
|
||||
pub track_total: Option<u32>,
|
||||
/// Disc number (for multi-disc albums)
|
||||
pub disc_number: Option<u32>,
|
||||
/// Total number of discs
|
||||
pub disc_total: Option<u32>,
|
||||
|
||||
// Technical audio properties
|
||||
/// Duration in seconds
|
||||
pub duration_secs: Option<u64>,
|
||||
/// Sample rate in Hz (e.g., 44100, 48000)
|
||||
pub sample_rate: Option<u32>,
|
||||
/// Number of audio channels (1 = mono, 2 = stereo, etc.)
|
||||
pub channels: Option<u8>,
|
||||
/// Bitrate in bits per second
|
||||
pub bitrate: Option<u32>,
|
||||
}
|
||||
|
||||
impl AudioFileMetadata {
|
||||
/// Extracts metadata from an audio file on disk.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Path to the audio file
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `AudioFileMetadata` on success, or a `lofty::error::LoftyError` if
|
||||
/// the file cannot be read or parsed.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use pmoflac::AudioFileMetadata;
|
||||
/// use std::path::Path;
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let metadata = AudioFileMetadata::from_file(Path::new("song.flac"))?;
|
||||
/// println!("Duration: {:?}s", metadata.duration_secs);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn from_file(path: &Path) -> Result<Self, lofty::error::LoftyError> {
|
||||
tracing::debug!(path = %path.display(), "Extracting metadata from file");
|
||||
|
||||
let tagged_file = Probe::open(path)?.options(ParseOptions::new()).read()?;
|
||||
|
||||
let metadata = Self::from_tagged_file(tagged_file);
|
||||
|
||||
tracing::debug!(
|
||||
path = %path.display(),
|
||||
title = ?metadata.title,
|
||||
artist = ?metadata.artist,
|
||||
duration_secs = ?metadata.duration_secs,
|
||||
"Metadata extracted successfully"
|
||||
);
|
||||
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
/// Extracts metadata from audio file bytes in memory.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `data` - Audio file data as a byte slice
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `AudioFileMetadata` on success, or a `lofty::error::LoftyError` if
|
||||
/// the data cannot be parsed.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use pmoflac::AudioFileMetadata;
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let audio_bytes: &[u8] = &[/* ... */];
|
||||
/// let metadata = AudioFileMetadata::from_bytes(audio_bytes)?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn from_bytes(data: &[u8]) -> Result<Self, lofty::error::LoftyError> {
|
||||
tracing::debug!(size = data.len(), "Extracting metadata from bytes");
|
||||
|
||||
let cursor = Cursor::new(data);
|
||||
let tagged_file = Probe::new(cursor)
|
||||
.guess_file_type()?
|
||||
.options(ParseOptions::new())
|
||||
.read()?;
|
||||
|
||||
Ok(Self::from_tagged_file(tagged_file))
|
||||
}
|
||||
|
||||
/// Internal helper to extract metadata from a lofty `TaggedFile`.
|
||||
fn from_tagged_file(tagged_file: lofty::file::TaggedFile) -> Self {
|
||||
let properties = tagged_file.properties();
|
||||
|
||||
// Try to get the primary tag, or fall back to the first available tag
|
||||
let tag = tagged_file
|
||||
.primary_tag()
|
||||
.or_else(|| tagged_file.first_tag());
|
||||
|
||||
// Initialize with technical properties
|
||||
let mut metadata = Self {
|
||||
title: None,
|
||||
artist: None,
|
||||
album: None,
|
||||
year: None,
|
||||
genre: None,
|
||||
track_number: None,
|
||||
track_total: None,
|
||||
disc_number: None,
|
||||
disc_total: None,
|
||||
duration_secs: Some(properties.duration().as_secs()),
|
||||
sample_rate: properties.sample_rate(),
|
||||
channels: properties.channels(),
|
||||
bitrate: properties.audio_bitrate(),
|
||||
};
|
||||
|
||||
// Extract artistic tags if available
|
||||
if let Some(tag) = tag {
|
||||
metadata.title = tag.title().map(|s| s.to_string());
|
||||
metadata.artist = tag.artist().map(|s| s.to_string());
|
||||
metadata.album = tag.album().map(|s| s.to_string());
|
||||
metadata.year = tag.year();
|
||||
metadata.genre = tag.genre().map(|s| s.to_string());
|
||||
metadata.track_number = tag.track();
|
||||
metadata.track_total = tag.track_total();
|
||||
metadata.disc_number = tag.disk();
|
||||
metadata.disc_total = tag.disk_total();
|
||||
} else {
|
||||
tracing::warn!("No tags found in audio file");
|
||||
}
|
||||
|
||||
metadata
|
||||
}
|
||||
|
||||
/// Returns a formatted duration string in H:MM:SS format (DIDL-Lite compatible).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use pmoflac::AudioFileMetadata;
|
||||
///
|
||||
/// let mut metadata = AudioFileMetadata {
|
||||
/// duration_secs: Some(3665), // 1 hour, 1 minute, 5 seconds
|
||||
/// title: None,
|
||||
/// artist: None,
|
||||
/// album: None,
|
||||
/// year: None,
|
||||
/// genre: None,
|
||||
/// track_number: None,
|
||||
/// track_total: None,
|
||||
/// disc_number: None,
|
||||
/// disc_total: None,
|
||||
/// sample_rate: None,
|
||||
/// channels: None,
|
||||
/// bitrate: None,
|
||||
/// };
|
||||
///
|
||||
/// assert_eq!(metadata.duration_formatted(), "1:01:05");
|
||||
/// ```
|
||||
pub fn duration_formatted(&self) -> String {
|
||||
match self.duration_secs {
|
||||
Some(secs) => {
|
||||
let hours = secs / 3600;
|
||||
let minutes = (secs % 3600) / 60;
|
||||
let seconds = secs % 60;
|
||||
format!("{}:{:02}:{:02}", hours, minutes, seconds)
|
||||
}
|
||||
None => "0:00:00".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a collection key in the format "artist:album" for grouping tracks.
|
||||
///
|
||||
/// This is useful for organizing tracks into albums in a music library.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use pmoflac::AudioFileMetadata;
|
||||
///
|
||||
/// let metadata = AudioFileMetadata {
|
||||
/// artist: Some("Pink Floyd".to_string()),
|
||||
/// album: Some("The Dark Side of the Moon".to_string()),
|
||||
/// title: None,
|
||||
/// year: None,
|
||||
/// genre: None,
|
||||
/// track_number: None,
|
||||
/// track_total: None,
|
||||
/// disc_number: None,
|
||||
/// disc_total: None,
|
||||
/// duration_secs: None,
|
||||
/// sample_rate: None,
|
||||
/// channels: None,
|
||||
/// bitrate: None,
|
||||
/// };
|
||||
///
|
||||
/// assert_eq!(metadata.collection_key(), "Pink Floyd:The Dark Side of the Moon");
|
||||
/// ```
|
||||
pub fn collection_key(&self) -> String {
|
||||
match (&self.artist, &self.album) {
|
||||
(Some(artist), Some(album)) => format!("{}:{}", artist, album),
|
||||
(Some(artist), None) => artist.clone(),
|
||||
(None, Some(album)) => album.clone(),
|
||||
(None, None) => "Unknown".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_duration_formatted() {
|
||||
let mut meta = AudioFileMetadata::default();
|
||||
|
||||
meta.duration_secs = Some(65);
|
||||
assert_eq!(meta.duration_formatted(), "0:01:05");
|
||||
|
||||
meta.duration_secs = Some(3665);
|
||||
assert_eq!(meta.duration_formatted(), "1:01:05");
|
||||
|
||||
meta.duration_secs = Some(0);
|
||||
assert_eq!(meta.duration_formatted(), "0:00:00");
|
||||
|
||||
meta.duration_secs = None;
|
||||
assert_eq!(meta.duration_formatted(), "0:00:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collection_key() {
|
||||
let mut meta = AudioFileMetadata::default();
|
||||
|
||||
meta.artist = Some("Artist".to_string());
|
||||
meta.album = Some("Album".to_string());
|
||||
assert_eq!(meta.collection_key(), "Artist:Album");
|
||||
|
||||
meta.album = None;
|
||||
assert_eq!(meta.collection_key(), "Artist");
|
||||
|
||||
meta.artist = None;
|
||||
meta.album = Some("Album".to_string());
|
||||
assert_eq!(meta.collection_key(), "Album");
|
||||
|
||||
meta.album = None;
|
||||
assert_eq!(meta.collection_key(), "Unknown");
|
||||
}
|
||||
}
|
||||
@@ -66,9 +66,9 @@ impl PcmFormat {
|
||||
384000,
|
||||
];
|
||||
if !STANDARD_RATES.contains(&self.sample_rate) {
|
||||
eprintln!(
|
||||
"Warning: non-standard sample rate {} Hz (valid but unusual)",
|
||||
self.sample_rate
|
||||
tracing::warn!(
|
||||
sample_rate = %self.sample_rate,
|
||||
"non-standard sample rate (valid but unusual)"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,18 +79,18 @@ impl PcmFormat {
|
||||
|
||||
// FLAC officially supports 4-32 bits/sample
|
||||
if self.bits_per_sample < 4 {
|
||||
eprintln!(
|
||||
"Warning: bits_per_sample={} is less than 4 (unusual for FLAC)",
|
||||
self.bits_per_sample
|
||||
tracing::warn!(
|
||||
bits_per_sample = %self.bits_per_sample,
|
||||
"bits_per_sample is less than 4 (unusual for FLAC)"
|
||||
);
|
||||
}
|
||||
|
||||
// Warn about common bit depths
|
||||
const COMMON_BIT_DEPTHS: &[u8] = &[8, 16, 24, 32];
|
||||
if !COMMON_BIT_DEPTHS.contains(&self.bits_per_sample) {
|
||||
eprintln!(
|
||||
"Warning: non-standard bit depth {} (valid but unusual)",
|
||||
self.bits_per_sample
|
||||
tracing::warn!(
|
||||
bits_per_sample = %self.bits_per_sample,
|
||||
"non-standard bit depth (valid but unusual)"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
64
pmoflac/src/prefixed_reader.rs
Normal file
64
pmoflac/src/prefixed_reader.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
//! Utility for reading from a prefix buffer followed by an underlying reader.
|
||||
//!
|
||||
//! This module provides `PrefixedReader`, which allows reading from an in-memory
|
||||
//! buffer first, then seamlessly continuing with an underlying async reader.
|
||||
//!
|
||||
//! This is particularly useful for format detection where we need to peek at the
|
||||
//! beginning of a stream before processing it.
|
||||
|
||||
use std::{
|
||||
cmp, io,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use tokio::io::{AsyncRead, ReadBuf};
|
||||
|
||||
/// An async reader that reads from a prefix buffer before delegating to an underlying reader.
|
||||
///
|
||||
/// This is useful when you've already consumed some bytes from a stream (e.g., for format
|
||||
/// detection) and need to replay them before continuing with the rest of the stream.
|
||||
pub(crate) struct PrefixedReader<R> {
|
||||
prefix: Vec<u8>,
|
||||
position: usize,
|
||||
reader: R,
|
||||
}
|
||||
|
||||
impl<R> PrefixedReader<R> {
|
||||
/// Creates a new `PrefixedReader` with the given prefix and underlying reader.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `prefix` - Bytes to read first before delegating to the reader
|
||||
/// * `reader` - The underlying async reader to use after the prefix is exhausted
|
||||
pub fn new(prefix: Vec<u8>, reader: R) -> Self {
|
||||
Self {
|
||||
prefix,
|
||||
position: 0,
|
||||
reader,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: AsyncRead + Unpin> AsyncRead for PrefixedReader<R> {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
// If we still have bytes in the prefix, read from there first
|
||||
if self.position < self.prefix.len() && buf.remaining() > 0 {
|
||||
let remaining = self.prefix.len() - self.position;
|
||||
let to_copy = cmp::min(remaining, buf.remaining());
|
||||
buf.put_slice(&self.prefix[self.position..self.position + to_copy]);
|
||||
self.position += to_copy;
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
// Prefix exhausted, delegate to underlying reader
|
||||
Pin::new(&mut self.reader).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Unpin> Unpin for PrefixedReader<R> {}
|
||||
unsafe impl<R: Send> Send for PrefixedReader<R> {}
|
||||
@@ -15,7 +15,8 @@ use tokio::io::{AsyncRead, AsyncReadExt, BufReader, ReadBuf};
|
||||
|
||||
use crate::{
|
||||
autodetect::{decode_audio_stream, DecodeAudioError, DecodedAudioStream},
|
||||
encode_flac_stream, EncoderOptions, FlacEncodedStream, FlacError, PcmFormat, StreamInfo,
|
||||
encode_flac_stream, prefixed_reader::PrefixedReader, EncoderOptions, FlacEncodedStream,
|
||||
FlacError, PcmFormat, StreamInfo,
|
||||
};
|
||||
|
||||
const READ_CHUNK: usize = 4096;
|
||||
@@ -306,43 +307,6 @@ fn parse_flac_stream_info(block: &[u8]) -> Option<StreamInfo> {
|
||||
})
|
||||
}
|
||||
|
||||
struct PrefixedReader<R> {
|
||||
prefix: Vec<u8>,
|
||||
position: usize,
|
||||
reader: R,
|
||||
}
|
||||
|
||||
impl<R> PrefixedReader<R> {
|
||||
fn new(prefix: Vec<u8>, reader: R) -> Self {
|
||||
Self {
|
||||
prefix,
|
||||
position: 0,
|
||||
reader,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<R: Send> Send for PrefixedReader<R> {}
|
||||
impl<R: Unpin> Unpin for PrefixedReader<R> {}
|
||||
|
||||
impl<R: AsyncRead + Unpin> AsyncRead for PrefixedReader<R> {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
if self.position < self.prefix.len() && buf.remaining() > 0 {
|
||||
let remaining = self.prefix.len() - self.position;
|
||||
let to_copy = remaining.min(buf.remaining());
|
||||
buf.put_slice(&self.prefix[self.position..self.position + to_copy]);
|
||||
self.position += to_copy;
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
Pin::new(&mut self.reader).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user