use super::*;
use bacnet_objects::file::FileWriteStart;
use bacnet_objects::traits::BACnetObject;
use bacnet_types::enums::FileAccessMethod as ObjectFileAccessMethod;
use bacnet_types::error::RemoteError;
fn invalid_file_access_method() -> Error {
Error::protocol(ErrorClass::SERVICES, ErrorCode::INVALID_FILE_ACCESS_METHOD)
}
fn inconsistent_object_type() -> Error {
Error::protocol(ErrorClass::SERVICES, ErrorCode::INCONSISTENT_OBJECT_TYPE)
}
fn validate_file_access_method(
object: &dyn BACnetObject,
expected: ObjectFileAccessMethod,
) -> Result<(), Error> {
let actual = match object.read_property(PropertyIdentifier::FILE_ACCESS_METHOD, None) {
Ok(PropertyValue::Enumerated(raw)) => raw,
_ => return Err(invalid_file_access_method()),
};
if actual != expected.to_raw() {
return Err(invalid_file_access_method());
}
Ok(())
}
fn invalid_file_start_position() -> Error {
Error::protocol(ErrorClass::SERVICES, ErrorCode::INVALID_FILE_START_POSITION)
}
fn file_access_denied() -> Error {
Error::protocol(ErrorClass::SERVICES, ErrorCode::FILE_ACCESS_DENIED)
}
fn unknown_object() -> Error {
Error::protocol(ErrorClass::OBJECT, ErrorCode::UNKNOWN_OBJECT)
}
fn ack_position(actual: u64) -> Result<i32, Error> {
i32::try_from(actual).map_err(|_| {
Error::Remote(RemoteError::Error {
class: ErrorClass::DEVICE,
code: ErrorCode::INTERNAL_ERROR,
})
})
}
pub fn handle_atomic_read_file(
db: &ObjectDatabase,
service_data: &[u8],
buf: &mut BytesMut,
) -> Result<(), Error> {
read_file(db, service_data, buf, None, |_, _| {}).map_err(|failure| match failure {
AtomicReadFileFailure::Service(error) => error,
AtomicReadFileFailure::Budget(_) => unreachable!("unconfigured read has no budget"),
})
}
#[derive(Debug)]
pub(crate) enum AtomicReadFileFailure {
Service(Error),
Budget(bacnet_types::enums::AbortReason),
}
impl From<Error> for AtomicReadFileFailure {
fn from(error: Error) -> Self {
Self::Service(error)
}
}
#[cfg(test)]
pub(crate) fn handle_atomic_read_file_budgeted(
db: &ObjectDatabase,
service_data: &[u8],
buf: &mut BytesMut,
budget: crate::server::AtomicReadFileBudget,
) -> Result<(), AtomicReadFileFailure> {
handle_atomic_read_file_observed(db, service_data, buf, budget, |_, _| {})
}
pub(crate) fn handle_atomic_read_file_observed(
db: &ObjectDatabase,
service_data: &[u8],
buf: &mut BytesMut,
budget: crate::server::AtomicReadFileBudget,
completed: impl FnOnce(ObjectIdentifier, &Result<(), Error>),
) -> Result<(), AtomicReadFileFailure> {
read_file(db, service_data, buf, Some(budget), completed)
}
fn read_file(
db: &ObjectDatabase,
service_data: &[u8],
buf: &mut BytesMut,
budget: Option<crate::server::AtomicReadFileBudget>,
completed: impl FnOnce(ObjectIdentifier, &Result<(), Error>),
) -> Result<(), AtomicReadFileFailure> {
let request = bacnet_services::file::AtomicReadFileRequest::decode(service_data)
.map_err(Error::into_request_reject)?;
let target = request.file_identifier;
let result = match execute_read_file(db, request, buf, budget) {
Ok(()) => Ok(()),
Err(AtomicReadFileFailure::Service(error)) => Err(error),
Err(failure @ AtomicReadFileFailure::Budget(_)) => return Err(failure),
};
completed(target, &result);
result.map_err(AtomicReadFileFailure::Service)
}
fn execute_read_file(
db: &ObjectDatabase,
request: bacnet_services::file::AtomicReadFileRequest,
buf: &mut BytesMut,
budget: Option<crate::server::AtomicReadFileBudget>,
) -> Result<(), AtomicReadFileFailure> {
use bacnet_services::common::MAX_DECODED_ITEMS;
use bacnet_services::file::{AtomicReadFileAck, FileAccessMethod, FileReadAckMethod};
if request.file_identifier.object_type() != ObjectType::FILE {
return Err(inconsistent_object_type().into());
}
let object = db
.get(&request.file_identifier)
.ok_or_else(unknown_object)?;
if object.file_storage_internal().is_none() {
return Err(file_access_denied().into());
}
let expected = match &request.access {
FileAccessMethod::Stream { .. } => ObjectFileAccessMethod::STREAM_ACCESS,
FileAccessMethod::Record { .. } => ObjectFileAccessMethod::RECORD_ACCESS,
};
validate_file_access_method(object, expected)?;
let storage = object
.file_storage_internal()
.ok_or_else(file_access_denied)?;
match request.access {
FileAccessMethod::Stream {
file_start_position,
requested_octet_count,
} => {
let start =
u64::try_from(file_start_position).map_err(|_| invalid_file_start_position())?;
if budget.is_some_and(|b| {
u64::from(requested_octet_count) > b.max_requested_stream_octets as u64
}) {
return Err(AtomicReadFileFailure::Budget(
bacnet_types::enums::AbortReason::OUT_OF_RESOURCES,
));
}
let read = storage.read_stream(start, u64::from(requested_octet_count))?;
let ack = AtomicReadFileAck {
end_of_file: read.end_of_file,
access: FileReadAckMethod::Stream {
file_start_position,
file_data: read.data,
},
};
encode_read_ack(&ack, buf, budget)?;
Ok(())
}
FileAccessMethod::Record {
file_start_record,
requested_record_count,
} => {
let start =
u64::try_from(file_start_record).map_err(|_| invalid_file_start_position())?;
if budget
.is_some_and(|b| u64::from(requested_record_count) > b.max_requested_records as u64)
{
return Err(AtomicReadFileFailure::Budget(
bacnet_types::enums::AbortReason::OUT_OF_RESOURCES,
));
}
let count = u64::from(requested_record_count).min(MAX_DECODED_ITEMS as u64);
let read = storage.read_records(start, count)?;
let ack = AtomicReadFileAck {
end_of_file: read.end_of_file,
access: FileReadAckMethod::Record {
file_start_record,
returned_record_count: read.records.len() as u32,
file_record_data: read.records,
},
};
encode_read_ack(&ack, buf, budget)?;
Ok(())
}
}
}
fn encode_read_ack(
ack: &bacnet_services::file::AtomicReadFileAck,
buf: &mut BytesMut,
budget: Option<crate::server::AtomicReadFileBudget>,
) -> Result<(), AtomicReadFileFailure> {
if budget.is_some_and(|b| ack.encoded_len_bounded(b.max_service_ack_bytes).is_none()) {
return Err(AtomicReadFileFailure::Budget(
bacnet_types::enums::AbortReason::BUFFER_OVERFLOW,
));
}
ack.encode(buf);
Ok(())
}
pub fn handle_atomic_write_file(
db: &mut ObjectDatabase,
service_data: &[u8],
buf: &mut BytesMut,
) -> Result<(), Error> {
write_file(db, service_data, buf, None, |_, _, _| {}).map_err(|failure| match failure {
AtomicWriteFileFailure::Service(error) => error,
AtomicWriteFileFailure::Budget => unreachable!("unconfigured write has no budget"),
})
}
#[derive(Debug)]
pub(crate) enum AtomicWriteFileFailure {
Service(Error),
Budget,
}
impl From<Error> for AtomicWriteFileFailure {
fn from(error: Error) -> Self {
Self::Service(error)
}
}
#[cfg(test)]
pub(crate) fn handle_atomic_write_file_budgeted(
db: &mut ObjectDatabase,
service_data: &[u8],
buf: &mut BytesMut,
budget: crate::server::AtomicWriteFileBudget,
) -> Result<(), AtomicWriteFileFailure> {
handle_atomic_write_file_observed(db, service_data, buf, budget, |_, _, _| {})
}
pub(crate) fn handle_atomic_write_file_observed(
db: &mut ObjectDatabase,
service_data: &[u8],
buf: &mut BytesMut,
budget: crate::server::AtomicWriteFileBudget,
completed: impl FnOnce(&mut ObjectDatabase, ObjectIdentifier, &Result<(), Error>),
) -> Result<(), AtomicWriteFileFailure> {
write_file(db, service_data, buf, Some(budget), completed)
}
fn write_file(
db: &mut ObjectDatabase,
service_data: &[u8],
buf: &mut BytesMut,
budget: Option<crate::server::AtomicWriteFileBudget>,
completed: impl FnOnce(&mut ObjectDatabase, ObjectIdentifier, &Result<(), Error>),
) -> Result<(), AtomicWriteFileFailure> {
use bacnet_services::file::{AtomicWriteFileRequest, FileWriteAccessMethod};
let request =
AtomicWriteFileRequest::decode(service_data).map_err(Error::into_request_reject)?;
if let FileWriteAccessMethod::Record {
record_count,
file_record_data,
..
} = &request.access
&& *record_count as usize != file_record_data.len()
{
return Err(Error::reject(RejectReason::MISSING_REQUIRED_PARAMETER).into());
}
let target = request.file_identifier;
let result = match execute_write_file(db, request, buf, budget) {
Ok(()) => Ok(()),
Err(AtomicWriteFileFailure::Service(error)) => Err(error),
Err(AtomicWriteFileFailure::Budget) => return Err(AtomicWriteFileFailure::Budget),
};
completed(db, target, &result);
result.map_err(AtomicWriteFileFailure::Service)
}
fn execute_write_file(
db: &mut ObjectDatabase,
request: bacnet_services::file::AtomicWriteFileRequest,
buf: &mut BytesMut,
budget: Option<crate::server::AtomicWriteFileBudget>,
) -> Result<(), AtomicWriteFileFailure> {
use bacnet_services::file::{AtomicWriteFileAck, FileWriteAccessMethod, FileWriteAckMethod};
if request.file_identifier.object_type() != ObjectType::FILE {
return Err(inconsistent_object_type().into());
}
let object = db
.get_mut(&request.file_identifier)
.ok_or_else(unknown_object)?;
if object.file_storage_internal().is_none() {
return Err(file_access_denied().into());
}
match object.read_property(PropertyIdentifier::READ_ONLY, None) {
Ok(PropertyValue::Boolean(false)) => {}
_ => return Err(file_access_denied().into()),
}
let expected = match &request.access {
FileWriteAccessMethod::Stream { .. } => ObjectFileAccessMethod::STREAM_ACCESS,
FileWriteAccessMethod::Record { .. } => ObjectFileAccessMethod::RECORD_ACCESS,
};
validate_file_access_method(object, expected)?;
let storage = object
.file_storage_internal_mut()
.ok_or_else(file_access_denied)?;
match request.access {
FileWriteAccessMethod::Stream {
file_start_position,
file_data,
} => {
let start = write_start(file_start_position)?;
if budget.is_some_and(|b| file_data.len() > b.max_stream_payload_octets) {
return Err(AtomicWriteFileFailure::Budget);
}
let actual = storage.write_stream(start, &file_data)?;
let ack = AtomicWriteFileAck {
access: FileWriteAckMethod::Stream {
file_start_position: ack_position(actual)?,
},
};
ack.encode(buf);
Ok(())
}
FileWriteAccessMethod::Record {
file_start_record,
file_record_data,
..
} => {
let start = write_start(file_start_record)?;
if budget.is_some_and(|b| {
file_record_data.len() > b.max_records
|| !record_payload_fits(
file_record_data.iter().map(Vec::len),
b.max_record_payload_bytes,
)
}) {
return Err(AtomicWriteFileFailure::Budget);
}
let actual = storage.write_records(start, &file_record_data)?;
let ack = AtomicWriteFileAck {
access: FileWriteAckMethod::Record {
file_start_record: ack_position(actual)?,
},
};
ack.encode(buf);
Ok(())
}
}
}
fn write_start(requested: i32) -> Result<FileWriteStart, Error> {
match requested {
-1 => Ok(FileWriteStart::Append),
position => u64::try_from(position)
.map(FileWriteStart::At)
.map_err(|_| invalid_file_start_position()),
}
}
fn record_payload_fits(lengths: impl IntoIterator<Item = usize>, cap: usize) -> bool {
let mut total = 0usize;
for len in lengths {
let Some(next) = total.checked_add(len).filter(|&next| next <= cap) else {
return false;
};
total = next;
}
true
}
#[cfg(test)]
mod write_budget_arithmetic {
use super::record_payload_fits;
#[test]
fn atomic_write_file_checked_sum_and_early_stop() {
assert!(record_payload_fits([0, usize::MAX], usize::MAX));
assert!(!record_payload_fits([usize::MAX, 1], usize::MAX));
assert!(record_payload_fits([2, 3, 0], 5));
assert!(!record_payload_fits([2, 3], 4));
assert!(!record_payload_fits(
std::iter::once(6).chain(std::iter::from_fn(|| panic!("past excess"))),
5
));
}
}