dat 4.6.1

DAT - Distributed Access Token
Documentation
//! DAT 통합 오류 코드 (error.pre2.md).
//!
//! 코드 문자열은 모든 공식 클라이언트와 CMS 서버가 공유하는 공개 계약이다. 메시지는 자유롭게 바꿔도 되지만
//! `code()` 가 돌려주는 문자열은 바꾸지 않는다.
//!
//! - 분류는 **원인**이다. "어느 함수에서 났는가"가 아니라 "무엇이 잘못됐는가"다.
//! - `*Unknown` 은 각 영역의 폴백 전용이다. "알 수 없는 X" 라는 뜻으로 쓰지 않는다.
//! - 하위 원인은 버리지 않고 [`DatError::cause`] 로 보존한다.

use std::error::Error;
use std::fmt;

/// 재시도 분류. 중간값을 두지 않는다 — 호출부가 분기할 수 없기 때문이다.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum DatRetry {
    /// 같은 입력으로 재시도하면 해소될 수 있다. 백오프 후 재시도한다.
    Transient,
    /// 설정·입력·배포를 고쳐야 한다. 재시도하지 않는다.
    Permanent,
    /// 오류가 아닌 상태 신호. 흐름 제어에만 쓴다.
    State,
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub enum DatError {
    // ---- TOKEN : DAT 토큰 문자열 ----
    /// 토큰 형식이 잘못됨 (파트 수·필드·인코딩·범위 초과 전부).
    TokenMalformed(&'static str),
    /// 토큰 만료. 형식 오류·서명 위조와 반드시 구분한다.
    TokenExpired,
    TokenUnknown(&'static str),

    // ---- CERT : 인증서 ----
    /// 인증서 형식이 잘못됨 (파트 수·필드·인코딩·시간 계산 오버플로).
    CertMalformed(&'static str),
    /// 인증서 최종 만료 (`start + duration + ttl < now`).
    CertExpired,
    /// 발급창이 아직 열리지 않음 (`now < start`).
    CertNotYetIssuable,
    /// 발급창이 닫힘. 검증만 가능하다.
    CertIssuanceEnded,
    /// 서명 개인키가 없는 인증서.
    CertVerifyOnly,
    /// 해당 CID의 인증서가 없음 (위조·오배포).
    CertNotFound,
    /// CID를 아직 동기화받지 못함. `CertNotFound` 와 다르다 — 기다리면 풀린다.
    CertNotSynced,
    /// import 목록 안에 CID 중복.
    CertDuplicateCid,
    CertUnknown(&'static str),

    // ---- SIG : 서명 ----
    /// 서명 검증 실패 — 위조·변조. **보안 이벤트.**
    SigMismatch,
    /// 서명 자체의 형식 오류 (빈 서명·길이 불일치·DER 변환 실패).
    SigMalformed(&'static str),
    /// 서명할 개인키가 없음 (verify-only 키로 sign 호출).
    SigKeyMissing,
    /// 서명·검증 연산이 실패함. **불일치가 아니다.**
    SigBackend(&'static str),
    SigUnknown(&'static str),

    // ---- CRYPTO : secure 페이로드 ----
    /// GCM 인증 태그 불일치 — 변조. **보안 이벤트.**
    CryptoTagMismatch,
    /// 암호문 길이가 규격 밖 (IV 미만·구현 한계 초과).
    CryptoDataInvalid(&'static str),
    /// 암·복호 연산이 실패함.
    CryptoBackend(&'static str),
    CryptoUnknown(&'static str),

    // ---- KEY : 키 재료 ----
    /// 키 재료가 무효 (길이 불일치·곡선 밖·인코딩 오류·쌍 불일치).
    KeyInvalid(&'static str),
    /// 이 알고리즘은 verify-only 를 지원하지 않음 (알고리즘의 구조적 한계).
    KeyVerifyOnlyUnsupported(String),
    KeyUnknown(&'static str),

    // ---- MANAGER : 매니저 보유 상태 ----
    /// 인증서를 하나도 보유하지 않음.
    ManagerNoCertificate,
    /// 발급 가능한 인증서가 없음. 사유는 `cause` 의 `Cert*` 코드로 전달한다.
    ManagerNoIssuableCertificate(Box<DatError>),
    /// 이미 해제된 객체를 사용. (명시적 수명 관리 포트용, rust 는 생성하지 않는다)
    ManagerDisposed,
    ManagerUnknown(&'static str),

    // ---- CMS : 서버 응답·전송 ----
    /// 서버에 도달할 수 없음 (DNS·연결 거부·TLS·타임아웃).
    CmsUnreachable(String),
    /// 인증 실패, 401 수신 — 토큰 설정 오류.
    CmsUnauthorized,
    /// 권한 부족, 403 수신.
    CmsForbidden,
    /// 엔드포인트 없음, 404 수신 — URL 설정 오류.
    CmsEndpointNotFound,
    /// 서버 내부 오류, 5xx 수신.
    CmsServerError(u16),
    /// 그 외 비-2xx.
    CmsHttpStatus(u16),
    /// 응답 본문이 프로토콜 위반 (구조·버전 줄 파싱 실패).
    CmsMalformed(&'static str),
    /// 받은 인증서를 적용하지 못함. 원인은 `cause` 로 보존한다.
    CmsImportFailed(Box<DatError>),
    /// 서버가 전체 재동기화를 지시.
    CmsVersionReset,
    /// 아직 한 번도 동기화하지 못함.
    CmsNotSynced,
    /// 이전 동기화가 진행 중이라 건너뜀. 오류가 아니다.
    CmsSyncInProgress,
    /// CMS 기능이 빌드에 포함되지 않음 (`dat_cms` feature 미활성).
    CmsNotSupported,
    CmsUnknown(String),

    // ---- CONFIG : 호출자가 넘긴 값 ----
    /// 지원하지 않는 알고리즘 이름.
    ConfigAlgUnsupported(String),
    /// CMS 서버 URI 가 규격 밖 (형식·스킴·경로·쿼리).
    ConfigUriInvalid(&'static str),
    /// 인자가 잘못됨 (null·범위 밖·타입 불일치·빈 값).
    ConfigArgumentInvalid(&'static str),
    ConfigUnknown(&'static str),

    // ---- INTERNAL : 실행 환경 ----
    /// 암호 백엔드나 런타임 API 가 없음 (배포·플랫폼 문제).
    InternalUnavailable(&'static str),
    /// 그 외 내부 실패 (메모리 할당·난수 생성·락·불변식 위반).
    InternalUnknown(&'static str),
}

impl DatError {
    /// 공개 계약인 오류 코드 문자열. 모든 공식 클라이언트에서 동일하다.
    pub fn code(&self) -> &'static str {
        use DatError::*;
        match self {
            TokenMalformed(_) => "DAT_TOKEN_MALFORMED",
            TokenExpired => "DAT_TOKEN_EXPIRED",
            TokenUnknown(_) => "DAT_TOKEN_UNKNOWN",

            CertMalformed(_) => "DAT_CERT_MALFORMED",
            CertExpired => "DAT_CERT_EXPIRED",
            CertNotYetIssuable => "DAT_CERT_NOT_YET_ISSUABLE",
            CertIssuanceEnded => "DAT_CERT_ISSUANCE_ENDED",
            CertVerifyOnly => "DAT_CERT_VERIFY_ONLY",
            CertNotFound => "DAT_CERT_NOT_FOUND",
            CertNotSynced => "DAT_CERT_NOT_SYNCED",
            CertDuplicateCid => "DAT_CERT_DUPLICATE_CID",
            CertUnknown(_) => "DAT_CERT_UNKNOWN",

            SigMismatch => "DAT_SIG_MISMATCH",
            SigMalformed(_) => "DAT_SIG_MALFORMED",
            SigKeyMissing => "DAT_SIG_KEY_MISSING",
            SigBackend(_) => "DAT_SIG_BACKEND",
            SigUnknown(_) => "DAT_SIG_UNKNOWN",

            CryptoTagMismatch => "DAT_CRYPTO_TAG_MISMATCH",
            CryptoDataInvalid(_) => "DAT_CRYPTO_DATA_INVALID",
            CryptoBackend(_) => "DAT_CRYPTO_BACKEND",
            CryptoUnknown(_) => "DAT_CRYPTO_UNKNOWN",

            KeyInvalid(_) => "DAT_KEY_INVALID",
            KeyVerifyOnlyUnsupported(_) => "DAT_KEY_VERIFY_ONLY_UNSUPPORTED",
            KeyUnknown(_) => "DAT_KEY_UNKNOWN",

            ManagerNoCertificate => "DAT_MANAGER_NO_CERTIFICATE",
            ManagerNoIssuableCertificate(_) => "DAT_MANAGER_NO_ISSUABLE_CERTIFICATE",
            ManagerDisposed => "DAT_MANAGER_DISPOSED",
            ManagerUnknown(_) => "DAT_MANAGER_UNKNOWN",

            CmsUnreachable(_) => "DAT_CMS_UNREACHABLE",
            CmsUnauthorized => "DAT_CMS_UNAUTHORIZED",
            CmsForbidden => "DAT_CMS_FORBIDDEN",
            CmsEndpointNotFound => "DAT_CMS_ENDPOINT_NOT_FOUND",
            CmsServerError(_) => "DAT_CMS_SERVER_ERROR",
            CmsHttpStatus(_) => "DAT_CMS_HTTP_STATUS",
            CmsMalformed(_) => "DAT_CMS_MALFORMED",
            CmsImportFailed(_) => "DAT_CMS_IMPORT_FAILED",
            CmsVersionReset => "DAT_CMS_VERSION_RESET",
            CmsNotSynced => "DAT_CMS_NOT_SYNCED",
            CmsSyncInProgress => "DAT_CMS_SYNC_IN_PROGRESS",
            CmsNotSupported => "DAT_CMS_NOT_SUPPORTED",
            CmsUnknown(_) => "DAT_CMS_UNKNOWN",

            ConfigAlgUnsupported(_) => "DAT_CONFIG_ALG_UNSUPPORTED",
            ConfigUriInvalid(_) => "DAT_CONFIG_URI_INVALID",
            ConfigArgumentInvalid(_) => "DAT_CONFIG_ARGUMENT_INVALID",
            ConfigUnknown(_) => "DAT_CONFIG_UNKNOWN",

            InternalUnavailable(_) => "DAT_INTERNAL_UNAVAILABLE",
            InternalUnknown(_) => "DAT_INTERNAL_UNKNOWN",
        }
    }

    /// 재시도 분류. 애매하면 `Permanent` 로 둔다 — 영구 오류에 대한 무한 재시도가
    /// 이 체계 이전의 실제 결함이었다.
    pub fn retry(&self) -> DatRetry {
        use DatError::*;
        match self {
            CertNotYetIssuable | CertNotSynced | ManagerNoCertificate | CmsUnreachable(_)
            | CmsServerError(_) | CmsNotSynced => DatRetry::Transient,

            CmsVersionReset | CmsSyncInProgress => DatRetry::State,

            ManagerNoIssuableCertificate(cause) => match **cause {
                CertNotYetIssuable => DatRetry::Transient,
                _ => DatRetry::Permanent,
            },

            _ => DatRetry::Permanent,
        }
    }

    /// 위조·변조 시도의 직접 증거. 다른 실패와 같은 경로로 로깅하지 않는다.
    #[inline]
    pub fn security_event(&self) -> bool {
        matches!(self, DatError::SigMismatch | DatError::CryptoTagMismatch)
    }

    /// 하위 원인. 체이닝을 버리지 않는다.
    pub fn cause(&self) -> Option<&DatError> {
        match self {
            DatError::ManagerNoIssuableCertificate(c) | DatError::CmsImportFailed(c) => Some(c),
            _ => None,
        }
    }
}

impl fmt::Display for DatError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use DatError::*;
        f.write_str(self.code())?;
        match self {
            TokenMalformed(d) | TokenUnknown(d) | CertMalformed(d) | CertUnknown(d)
            | SigMalformed(d) | SigBackend(d) | SigUnknown(d) | CryptoDataInvalid(d)
            | CryptoBackend(d) | CryptoUnknown(d) | KeyInvalid(d) | KeyUnknown(d)
            | ManagerUnknown(d) | CmsMalformed(d) | ConfigUriInvalid(d)
            | ConfigArgumentInvalid(d) | ConfigUnknown(d) | InternalUnavailable(d)
            | InternalUnknown(d) => write!(f, ": {d}"),

            KeyVerifyOnlyUnsupported(s) | CmsUnreachable(s) | CmsUnknown(s)
            | ConfigAlgUnsupported(s) => write!(f, ": {s}"),

            CmsServerError(s) | CmsHttpStatus(s) => write!(f, ": http {s}"),

            ManagerNoIssuableCertificate(c) | CmsImportFailed(c) => write!(f, ": {c}"),

            _ => Ok(()),
        }
    }
}

impl Error for DatError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        self.cause().map(|c| c as &(dyn Error + 'static))
    }
}

/// 이미 `Dat` 인 값을 `TryInto<Dat>` 로 넘기면 실패할 수 없다(`Infallible`).
/// 그 경로도 코드 보존 시그니처를 그대로 통과시키기 위한 변환이다.
impl From<std::convert::Infallible> for DatError {
    fn from(_: std::convert::Infallible) -> Self {
        DatError::InternalUnknown("unreachable: infallible conversion failed")
    }
}