ippper 0.6.0

A library for building simple ipp servers
Documentation
use crate::error::IppError;
use crate::result::IppResult;
use anyhow;
use http::request::Parts as ReqParts;
use ipp::attribute::IppAttribute;
use ipp::model::{DelimiterTag, IppVersion, Operation, StatusCode};
use ipp::request::IppRequestResponse;
use ipp::value::{IppString, IppTextValue, IppValue};
use num_traits::FromPrimitive;

fn operation_not_supported() -> anyhow::Error {
    anyhow::Error::new(IppError {
        code: StatusCode::ServerErrorOperationNotSupported,
        msg: StatusCode::ServerErrorOperationNotSupported.to_string(),
    })
}

pub trait IppService: Send + Sync {
    fn print_job(
        &self,
        _head: ReqParts,
        _req: IppRequestResponse,
    ) -> impl futures::Future<Output = IppResult> + Send {
        futures::future::ready(Err(operation_not_supported()))
    }

    fn print_uri(
        &self,
        _head: ReqParts,
        _req: IppRequestResponse,
    ) -> impl futures::Future<Output = IppResult> + Send {
        futures::future::ready(Err(operation_not_supported()))
    }

    fn validate_job(
        &self,
        _head: ReqParts,
        _req: IppRequestResponse,
    ) -> impl futures::Future<Output = IppResult> + Send {
        futures::future::ready(Err(operation_not_supported()))
    }

    fn create_job(
        &self,
        _head: ReqParts,
        _req: IppRequestResponse,
    ) -> impl futures::Future<Output = IppResult> + Send {
        futures::future::ready(Err(operation_not_supported()))
    }

    fn send_document(
        &self,
        _head: ReqParts,
        _req: IppRequestResponse,
    ) -> impl futures::Future<Output = IppResult> + Send {
        futures::future::ready(Err(operation_not_supported()))
    }

    fn send_uri(
        &self,
        _head: ReqParts,
        _req: IppRequestResponse,
    ) -> impl futures::Future<Output = IppResult> + Send {
        futures::future::ready(Err(operation_not_supported()))
    }

    fn cancel_job(
        &self,
        _head: ReqParts,
        _req: IppRequestResponse,
    ) -> impl futures::Future<Output = IppResult> + Send {
        futures::future::ready(Err(operation_not_supported()))
    }

    fn get_job_attributes(
        &self,
        _head: ReqParts,
        _req: IppRequestResponse,
    ) -> impl futures::Future<Output = IppResult> + Send {
        futures::future::ready(Err(operation_not_supported()))
    }

    fn get_jobs(
        &self,
        _head: ReqParts,
        _req: IppRequestResponse,
    ) -> impl futures::Future<Output = IppResult> + Send {
        futures::future::ready(Err(operation_not_supported()))
    }

    fn get_printer_attributes(
        &self,
        _head: ReqParts,
        _req: IppRequestResponse,
    ) -> impl futures::Future<Output = IppResult> + Send {
        futures::future::ready(Err(operation_not_supported()))
    }

    fn hold_job(
        &self,
        _head: ReqParts,
        _req: IppRequestResponse,
    ) -> impl futures::Future<Output = IppResult> + Send {
        futures::future::ready(Err(operation_not_supported()))
    }

    fn release_job(
        &self,
        _head: ReqParts,
        _req: IppRequestResponse,
    ) -> impl futures::Future<Output = IppResult> + Send {
        futures::future::ready(Err(operation_not_supported()))
    }

    fn restart_job(
        &self,
        _head: ReqParts,
        _req: IppRequestResponse,
    ) -> impl futures::Future<Output = IppResult> + Send {
        futures::future::ready(Err(operation_not_supported()))
    }

    fn pause_printer(
        &self,
        _head: ReqParts,
        _req: IppRequestResponse,
    ) -> impl futures::Future<Output = IppResult> + Send {
        futures::future::ready(Err(operation_not_supported()))
    }

    fn resume_printer(
        &self,
        _head: ReqParts,
        _req: IppRequestResponse,
    ) -> impl futures::Future<Output = IppResult> + Send {
        futures::future::ready(Err(operation_not_supported()))
    }

    fn purge_jobs(
        &self,
        _head: ReqParts,
        _req: IppRequestResponse,
    ) -> impl futures::Future<Output = IppResult> + Send {
        futures::future::ready(Err(operation_not_supported()))
    }

    fn version(&self) -> IppVersion {
        IppVersion::v1_1()
    }

    fn check_version(&self, req: &IppRequestResponse) -> bool {
        let version = req.header().version.0;
        version <= self.version().0
    }

    fn build_error_response(
        &self,
        version: IppVersion,
        req_id: u32,
        error: anyhow::Error,
    ) -> IppRequestResponse {
        let ipp_error = match error.downcast_ref::<IppError>() {
            Some(e) => e.clone(),
            None => IppError {
                code: StatusCode::ServerErrorInternalError,
                msg: error.to_string(),
            },
        };
        // SAFETY: IppRequestResponse::new_response should not fail for any version, status code or request id.
        // It only spread the error when some internal invariant is violated (e.g. invalid header automatically
        // appended by the library), which should not happen in this case.
        let mut resp = IppRequestResponse::new_response(version, ipp_error.code, req_id)
            .expect("failed to build IPP error response");
        let mut error_message_string = ipp_error.msg.clone();
        if error_message_string.len() > IppString::max() {
            error_message_string
                .truncate(error_message_string.floor_char_boundary(IppString::max()));
        }
        let error_message: IppTextValue = error_message_string
            .try_into()
            .unwrap_or_else(|_| "Error message not available".try_into().unwrap());
        resp.attributes_mut().add(
            DelimiterTag::OperationAttributes,
            IppAttribute::new(
                IppAttribute::STATUS_MESSAGE.try_into().unwrap(),
                IppValue::TextWithoutLanguage(error_message),
            ),
        );
        resp
    }

    fn handle_request(
        &self,
        head: ReqParts,
        req: IppRequestResponse,
    ) -> impl futures::Future<Output = IppRequestResponse> + Send {
        async {
            let req_id = req.header().request_id;
            if !self.check_version(&req) {
                return self.build_error_response(
                    self.version(),
                    req_id,
                    IppError {
                        code: StatusCode::ServerErrorVersionNotSupported,
                        msg: StatusCode::ServerErrorVersionNotSupported.to_string(),
                    }
                    .into(),
                );
            }
            let version = req.header().version;
            match Operation::from_u16(req.header().operation_or_status) {
                Some(op) => match op {
                    Operation::PrintJob => self.print_job(head, req).await,
                    Operation::PrintUri => self.print_uri(head, req).await,
                    Operation::ValidateJob => self.validate_job(head, req).await,
                    Operation::CreateJob => self.create_job(head, req).await,
                    Operation::SendDocument => self.send_document(head, req).await,
                    Operation::SendUri => self.send_uri(head, req).await,
                    Operation::CancelJob => self.cancel_job(head, req).await,
                    Operation::GetJobAttributes => self.get_job_attributes(head, req).await,
                    Operation::GetJobs => self.get_jobs(head, req).await,
                    Operation::GetPrinterAttributes => self.get_printer_attributes(head, req).await,
                    Operation::HoldJob => self.hold_job(head, req).await,
                    Operation::ReleaseJob => self.release_job(head, req).await,
                    Operation::RestartJob => self.restart_job(head, req).await,
                    Operation::PausePrinter => self.pause_printer(head, req).await,
                    Operation::ResumePrinter => self.resume_printer(head, req).await,
                    Operation::PurgeJobs => self.purge_jobs(head, req).await,
                    _ => Err(operation_not_supported()),
                },
                None => Err(operation_not_supported()),
            }
            .unwrap_or_else(|error| self.build_error_response(version, req_id, error))
        }
    }
}