use std::collections::HashMap;
use std::path::{Path, PathBuf};
use rskit_errors::{AppError, AppResult, ErrorCode};
use crate::{DataPayload, DatasetLimits, Label, MediaType};
pub trait DatasetItem: Send + 'static {
fn label(&self) -> Label {
Label::Real
}
fn source_offset(&self) -> Option<usize> {
None
}
}
#[derive(Debug, Clone)]
pub struct DataItem {
payload: DataPayload,
pub label: Label,
pub media_type: MediaType,
pub source_name: String,
pub extension: String,
pub metadata: HashMap<String, String>,
source_offset: Option<usize>,
}
impl DataItem {
pub fn new(
bytes: Vec<u8>,
label: Label,
media_type: MediaType,
source_name: impl Into<String>,
) -> AppResult<Self> {
Self::new_bytes(bytes, label, media_type, source_name)
}
pub fn new_bytes(
bytes: Vec<u8>,
label: Label,
media_type: MediaType,
source_name: impl Into<String>,
) -> AppResult<Self> {
Self::new_bytes_with_limits(
bytes,
label,
media_type,
source_name,
&DatasetLimits::default(),
)
}
pub fn new_bytes_with_limits(
bytes: Vec<u8>,
label: Label,
media_type: MediaType,
source_name: impl Into<String>,
limits: &DatasetLimits,
) -> AppResult<Self> {
Ok(Self {
payload: DataPayload::bytes(bytes, limits)?,
label,
media_type,
source_name: source_name.into(),
extension: ".jpg".to_string(),
metadata: HashMap::new(),
source_offset: None,
})
}
#[must_use]
pub fn new_file(
path: impl Into<PathBuf>,
label: Label,
media_type: MediaType,
source_name: impl Into<String>,
) -> Self {
Self {
payload: DataPayload::file(path),
label,
media_type,
source_name: source_name.into(),
extension: ".bin".to_string(),
metadata: HashMap::new(),
source_offset: None,
}
}
#[must_use]
pub fn payload(&self) -> &DataPayload {
&self.payload
}
pub fn try_with_payload(
mut self,
payload: DataPayload,
limits: &DatasetLimits,
) -> AppResult<Self> {
if payload.is_bytes() && payload.len()? > limits.max_in_memory_bytes as u64 {
return Err(AppError::new(
ErrorCode::InvalidInput,
format!(
"in-memory dataset payload exceeds max_in_memory_bytes={}",
limits.max_in_memory_bytes
),
));
}
self.payload = payload;
Ok(self)
}
#[must_use]
pub fn source_offset(&self) -> Option<usize> {
self.source_offset
}
#[must_use]
pub fn with_source_offset(mut self, offset: usize) -> Self {
self.source_offset = Some(offset);
self
}
#[must_use]
pub fn with_extension(mut self, ext: impl Into<String>) -> Self {
self.extension = ext.into();
self
}
#[must_use]
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn validate(&self, limits: &DatasetLimits) -> AppResult<()> {
validate_extension(&self.extension)?;
let len = self.payload.len()?;
if self.payload.is_bytes() && len > limits.max_in_memory_bytes as u64 {
return Err(AppError::new(
ErrorCode::InvalidInput,
format!(
"in-memory dataset payload is {len} bytes, exceeding max_in_memory_bytes={}",
limits.max_in_memory_bytes
),
));
}
Ok(())
}
pub fn write_to_path(&self, path: &Path, limits: &DatasetLimits) -> AppResult<u64> {
self.validate(limits)?;
self.payload.write_to_path(path, limits)
}
}
fn validate_extension(extension: &str) -> AppResult<()> {
let extension = extension.trim_start_matches('.');
if extension.is_empty()
|| extension.contains('/')
|| extension.contains('\\')
|| extension == "."
|| extension == ".."
|| extension.contains("..")
{
return Err(AppError::new(
ErrorCode::InvalidInput,
format!("invalid dataset item extension: {extension:?}"),
));
}
rskit_validation::input::validate_safe_path(extension)
}
impl crate::DatasetItem for DataItem {
fn label(&self) -> Label {
self.label
}
fn source_offset(&self) -> Option<usize> {
self.source_offset
}
}