use core::convert::TryFrom;
use std::time::Duration;
use api::heddle::api::v1alpha1::{
HostedGrant, HostedNamespace, HostedObjectType, HostedRepository, ObjectAvailabilityStatus,
ObjectDescriptor, RepositoryRef, StateAttachmentKind as ProtoStateAttachmentKind,
StateId as ProtoStateId, TransferCheckpoint, TransportMode, repository_ref::Reference,
};
use base64::Engine as _;
use cli_shared::ClientConfig;
use objects::object::{ContentHash, StateAttachmentId, StateAttachmentKind, StateId};
use wire::{ObjectId, ObjectInfo, ObjectType, ProtocolError};
use super::HostedError;
#[derive(Debug, Clone)]
pub(crate) struct HostedTransportPolicy {
pub chunk_size: usize,
pub max_inflight_objects: usize,
pub resume_attempts: usize,
pub provider_global_concurrency: usize,
pub provider_per_endpoint_concurrency: usize,
pub provider_max_inflight_bytes: usize,
pub provider_stall_timeout: Duration,
}
impl HostedTransportPolicy {
pub fn from_client_config(config: &ClientConfig) -> Self {
let chunk_size = config.chunk_size.max(1);
let max_inflight_objects = (chunk_size / (16 * 1024)).clamp(1, 16);
let provider_global_concurrency = config.provider_global_concurrency.clamp(1, 64);
Self {
chunk_size,
max_inflight_objects,
resume_attempts: 2,
provider_global_concurrency,
provider_per_endpoint_concurrency: config
.provider_per_endpoint_concurrency
.clamp(1, provider_global_concurrency),
provider_max_inflight_bytes: config.provider_max_inflight_bytes.max(1),
provider_stall_timeout: Duration::from_secs(config.provider_stall_timeout_secs.max(1)),
}
}
pub fn transfer_checkpoint_with_mode(
&self,
transfer_id: impl Into<String>,
mode: TransportMode,
chunk_index: u32,
resume_offset: u64,
is_complete: bool,
) -> TransferCheckpoint {
TransferCheckpoint {
transfer_id: transfer_id.into(),
transport_mode: mode as i32,
resume_offset,
chunk_index,
checkpoint: Vec::new(),
is_complete,
}
}
}
pub(super) fn attachment_kind_to_proto(kind: StateAttachmentKind) -> ProtoStateAttachmentKind {
match kind {
StateAttachmentKind::Context => ProtoStateAttachmentKind::Context,
StateAttachmentKind::RiskSignals => ProtoStateAttachmentKind::RiskSignals,
StateAttachmentKind::ReviewSignatures => ProtoStateAttachmentKind::ReviewSignatures,
StateAttachmentKind::Discussions => ProtoStateAttachmentKind::Discussions,
StateAttachmentKind::StructuredConflicts => ProtoStateAttachmentKind::StructuredConflicts,
StateAttachmentKind::SemanticIndex => ProtoStateAttachmentKind::SemanticIndex,
StateAttachmentKind::Signature => ProtoStateAttachmentKind::Signature,
}
}
fn attachment_kind_from_proto(kind: ProtoStateAttachmentKind) -> Option<StateAttachmentKind> {
match kind {
ProtoStateAttachmentKind::Unspecified => None,
ProtoStateAttachmentKind::Context => Some(StateAttachmentKind::Context),
ProtoStateAttachmentKind::RiskSignals => Some(StateAttachmentKind::RiskSignals),
ProtoStateAttachmentKind::ReviewSignatures => Some(StateAttachmentKind::ReviewSignatures),
ProtoStateAttachmentKind::Discussions => Some(StateAttachmentKind::Discussions),
ProtoStateAttachmentKind::StructuredConflicts => {
Some(StateAttachmentKind::StructuredConflicts)
}
ProtoStateAttachmentKind::SemanticIndex => Some(StateAttachmentKind::SemanticIndex),
ProtoStateAttachmentKind::Signature => Some(StateAttachmentKind::Signature),
}
}
pub(super) fn parse_descriptor_to_info(
descriptor: ObjectDescriptor,
) -> Result<ObjectInfo, ProtocolError> {
let obj_type = parse_object_type(descriptor.object_type)?;
let attachment_kind = if obj_type == ObjectType::StateAttachment {
let proto_kind = ProtoStateAttachmentKind::try_from(descriptor.attachment_kind)
.unwrap_or(ProtoStateAttachmentKind::Unspecified);
Some(attachment_kind_from_proto(proto_kind).ok_or_else(|| {
ProtocolError::InvalidState(
"state attachment descriptor is missing attachment_kind (UNSPECIFIED)".to_string(),
)
})?)
} else {
None
};
let id = parse_object_id(&descriptor.id, obj_type, attachment_kind)?;
Ok(ObjectInfo {
id,
obj_type,
size: 0,
delta_base: None,
})
}
pub(super) fn decode_blob_content(
content: String,
is_binary: bool,
) -> Result<Vec<u8>, ProtocolError> {
if is_binary {
base64::engine::general_purpose::STANDARD
.decode(content.as_bytes())
.map_err(|err| ProtocolError::Serialization(err.to_string()))
} else {
Ok(content.into_bytes())
}
}
pub(super) fn parse_object_id(
value: &str,
obj_type: ObjectType,
attachment_kind: Option<StateAttachmentKind>,
) -> Result<ObjectId, ProtocolError> {
match obj_type {
ObjectType::State | ObjectType::StateVisibility => {
Ok(ObjectId::StateId(StateId::parse(value).map_err(|err| {
ProtocolError::InvalidState(err.to_string())
})?))
}
ObjectType::StateAttachment => {
let (state, attachment) = value.split_once(':').ok_or_else(|| {
ProtocolError::InvalidState("invalid state attachment locator".to_string())
})?;
let kind = attachment_kind.ok_or_else(|| {
ProtocolError::InvalidState(
"state attachment descriptor is missing attachment_kind".to_string(),
)
})?;
Ok(ObjectId::StateAttachment {
state: StateId::parse(state)
.map_err(|err| ProtocolError::InvalidState(err.to_string()))?,
id: StateAttachmentId::from_hash(
ContentHash::from_hex(attachment)
.map_err(|err| ProtocolError::InvalidState(err.to_string()))?,
),
kind,
})
}
ObjectType::Blob | ObjectType::Tree | ObjectType::Action | ObjectType::Redaction => {
Ok(ObjectId::Hash(ContentHash::from_hex(value).map_err(
|err| ProtocolError::InvalidState(err.to_string()),
)?))
}
}
}
pub(super) fn parse_object_type(value: i32) -> Result<ObjectType, ProtocolError> {
match HostedObjectType::try_from(value).unwrap_or_default() {
HostedObjectType::Blob => Ok(ObjectType::Blob),
HostedObjectType::Tree => Ok(ObjectType::Tree),
HostedObjectType::State => Ok(ObjectType::State),
HostedObjectType::Action => Ok(ObjectType::Action),
HostedObjectType::Redaction => Ok(ObjectType::Redaction),
HostedObjectType::StateVisibility => Ok(ObjectType::StateVisibility),
HostedObjectType::StateAttachment => Ok(ObjectType::StateAttachment),
HostedObjectType::Unspecified => Err(ProtocolError::InvalidState(
"object descriptor is missing object_type".to_string(),
)),
}
}
fn object_type_to_proto(obj_type: ObjectType) -> HostedObjectType {
match obj_type {
ObjectType::Blob => HostedObjectType::Blob,
ObjectType::Tree => HostedObjectType::Tree,
ObjectType::State => HostedObjectType::State,
ObjectType::Action => HostedObjectType::Action,
ObjectType::Redaction => HostedObjectType::Redaction,
ObjectType::StateVisibility => HostedObjectType::StateVisibility,
ObjectType::StateAttachment => HostedObjectType::StateAttachment,
}
}
pub(super) fn to_proto_object_info(info: &ObjectInfo) -> ObjectDescriptor {
object_descriptor_with_status(info, ObjectAvailabilityStatus::Present, "")
}
pub(super) fn object_descriptor_with_status(
info: &ObjectInfo,
availability_status: ObjectAvailabilityStatus,
availability_note: impl Into<String>,
) -> ObjectDescriptor {
let attachment_kind = match &info.id {
ObjectId::StateAttachment { kind, .. } => attachment_kind_to_proto(*kind),
ObjectId::Hash(_) | ObjectId::StateId(_) => ProtoStateAttachmentKind::Unspecified,
};
ObjectDescriptor {
id: match &info.id {
ObjectId::Hash(hash) => hash.to_hex(),
ObjectId::StateId(state_id) => state_id.to_string_full(),
ObjectId::StateAttachment { state, id, kind: _ } => {
format!("{}:{}", state.to_string_full(), id.as_hash().to_hex())
}
},
object_type: object_type_to_proto(info.obj_type) as i32,
availability_status: availability_status as i32,
availability_note: availability_note.into(),
attachment_kind: attachment_kind as i32,
}
}
pub(super) fn transport_mode_name(mode: i32) -> &'static str {
match TransportMode::try_from(mode).unwrap_or(TransportMode::Unspecified) {
TransportMode::NativePack => "native-pack",
TransportMode::Unspecified => "unspecified",
}
}
pub(super) fn descriptor_id(descriptor: &ObjectDescriptor) -> (String, i32) {
(descriptor.id.clone(), descriptor.object_type)
}
pub(super) fn descriptor_id_from_info(info: &ObjectInfo) -> (String, i32) {
let id = match &info.id {
ObjectId::Hash(hash) => hash.to_hex(),
ObjectId::StateId(state_id) => state_id.to_string_full(),
ObjectId::StateAttachment { state, id, kind: _ } => {
format!("{}:{}", state.to_string_full(), id.as_hash().to_hex())
}
};
(id, object_type_to_proto(info.obj_type) as i32)
}
pub(super) fn hosted_to_protocol_error(error: HostedError) -> ProtocolError {
use api::heddle::api::v1alpha1::CallFailureCode;
match error {
HostedError::Call {
code,
message,
error,
} => {
if let Some(error) = error {
return ProtocolError::RemoteFailure {
code: remote_failure_code(code),
message,
details: vec![remote_failure_detail(*error)],
};
}
match code {
CallFailureCode::Unauthenticated | CallFailureCode::PermissionDenied => {
ProtocolError::AuthorizationFailed(message)
}
CallFailureCode::NotFound => ProtocolError::ObjectNotFound(message),
CallFailureCode::AlreadyExists => ProtocolError::AlreadyExists(message),
CallFailureCode::InvalidArgument | CallFailureCode::FailedPrecondition => {
ProtocolError::InvalidState(message)
}
_ => ProtocolError::RemoteFailure {
code: remote_failure_code(code),
message,
details: Vec::new(),
},
}
}
HostedError::Decode(error) => ProtocolError::Serialization(error.to_string()),
HostedError::Transport(message) => ProtocolError::Io(std::io::Error::other(message)),
error => ProtocolError::Remote(error.to_string()),
}
}
fn remote_failure_code(
code: api::heddle::api::v1alpha1::CallFailureCode,
) -> wire::RemoteFailureCode {
use api::heddle::api::v1alpha1::CallFailureCode as Api;
use wire::RemoteFailureCode as Wire;
match code {
Api::Unspecified => Wire::Unspecified,
Api::Cancelled => Wire::Cancelled,
Api::Unknown => Wire::Unknown,
Api::InvalidArgument => Wire::InvalidArgument,
Api::DeadlineExceeded => Wire::DeadlineExceeded,
Api::NotFound => Wire::NotFound,
Api::AlreadyExists => Wire::AlreadyExists,
Api::PermissionDenied => Wire::PermissionDenied,
Api::ResourceExhausted => Wire::ResourceExhausted,
Api::FailedPrecondition => Wire::FailedPrecondition,
Api::Aborted => Wire::Aborted,
Api::OutOfRange => Wire::OutOfRange,
Api::Unimplemented => Wire::Unimplemented,
Api::Internal => Wire::Internal,
Api::Unavailable => Wire::Unavailable,
Api::DataLoss => Wire::DataLoss,
Api::Unauthenticated => Wire::Unauthenticated,
}
}
fn remote_duration(value: prost_types::Duration) -> wire::RemoteDuration {
wire::RemoteDuration {
seconds: value.seconds,
nanos: value.nanos,
}
}
fn remote_cursor(value: api::heddle::api::v1alpha1::CursorFailure) -> wire::RemoteCursorFailure {
use api::heddle::api::v1alpha1::cursor_failure::Reason as Api;
use wire::RemoteCursorReason as Wire;
let reason = match value.reason() {
Api::Unspecified => Wire::Unspecified,
Api::Stale => Wire::Stale,
Api::Expired => Wire::Expired,
};
wire::RemoteCursorFailure {
reason,
expired_at: value.expired_at.map(|timestamp| wire::RemoteTimestamp {
seconds: timestamp.seconds,
nanos: timestamp.nanos,
}),
restart_cursor: value.restart_cursor,
}
}
fn remote_failure_detail(
detail: api::heddle::api::v1alpha1::ErrorDetail,
) -> wire::RemoteFailureDetail {
use api::heddle::api::v1alpha1::error_detail::Context;
use prost::Message as _;
let encoded = detail.encode_to_vec();
match detail.context {
Some(Context::Retry(value)) => wire::RemoteFailureDetail::Retry {
retry_after: value.retry_after.map(remote_duration),
},
Some(Context::Conflict(value)) => wire::RemoteFailureDetail::Conflict {
resource: value.resource,
expected_version: value.expected_version,
actual_version: value.actual_version,
},
Some(Context::Cursor(value)) => wire::RemoteFailureDetail::Cursor(remote_cursor(value)),
Some(Context::Capability(value)) => wire::RemoteFailureDetail::CapabilityRequirement {
capabilities: value.capabilities,
},
Some(Context::Policy(value)) => wire::RemoteFailureDetail::PolicyDenial {
policy_id: value.policy_id,
rule: value.rule,
human_verification_can_override: value.human_verification_can_override,
},
Some(Context::HumanVerification(_)) | None => wire::RemoteFailureDetail::Unknown {
type_url: "type.googleapis.com/heddle.api.v1alpha1.ErrorDetail".to_string(),
value: encoded,
},
}
}
pub(super) fn repository_ref(path: &str) -> Option<RepositoryRef> {
Some(RepositoryRef {
reference: Some(Reference::CanonicalPath(path.to_string())),
})
}
pub(crate) fn repository_ref_path(repository: &RepositoryRef) -> Option<&str> {
match repository.reference.as_ref() {
Some(Reference::HostedId(id) | Reference::CanonicalPath(id)) if !id.is_empty() => Some(id),
None => None,
_ => None,
}
}
pub(super) fn proto_state_id(state_id: StateId) -> Option<ProtoStateId> {
Some(ProtoStateId {
value: state_id.as_bytes().to_vec(),
})
}
pub(super) fn parse_proto_state_id(
state_id: Option<ProtoStateId>,
) -> Result<Option<StateId>, ProtocolError> {
state_id
.map(|state_id| {
let value: [u8; 32] = state_id.value.try_into().map_err(|value: Vec<u8>| {
ProtocolError::InvalidState(format!(
"state ID must be 32 bytes, got {}",
value.len()
))
})?;
Ok(StateId::from_bytes(value))
})
.transpose()
}
pub(super) fn to_protocol_namespace(namespace: HostedNamespace) -> wire::HostedNamespaceInfo {
use api::heddle::api::v1alpha1::NamespaceKind;
let kind = match NamespaceKind::try_from(namespace.kind).unwrap_or(NamespaceKind::Unspecified) {
NamespaceKind::User => "user",
NamespaceKind::Org => "namespace",
NamespaceKind::Team => "team",
NamespaceKind::Unspecified => "",
};
wire::HostedNamespaceInfo {
namespace_id: namespace.namespace_id,
kind: kind.to_string(),
slug: namespace.slug,
parent_id: (!namespace.parent_id.is_empty()).then_some(namespace.parent_id),
display_name: (!namespace.display_name.is_empty()).then_some(namespace.display_name),
full_path: namespace.full_path,
}
}
pub(super) fn to_protocol_repository(repository: HostedRepository) -> wire::HostedRepositoryInfo {
wire::HostedRepositoryInfo {
repo_id: repository.repo_id,
namespace_id: repository.namespace_id,
slug: repository.slug,
path: repository.path.into(),
full_path: repository.full_path,
}
}
pub(super) fn to_protocol_grant(grant: HostedGrant) -> wire::HostedGrantInfo {
use api::heddle::api::v1alpha1::grant_target_ref::Target;
let (namespace_path, repo_path) = match grant.target.and_then(|t| t.target) {
Some(Target::NamespacePath(p)) if !p.is_empty() => (Some(p), None),
Some(Target::RepoPath(p)) => (None, repository_ref_path(&p).map(ToOwned::to_owned)),
_ => (None, None),
};
wire::HostedGrantInfo {
subject: grant.subject,
role: hosted_role_proto_to_string(grant.role),
namespace_path,
repo_path,
}
}
pub(super) fn hosted_role_proto_to_string(role: i32) -> String {
use api::heddle::api::v1alpha1::HostedRole;
match HostedRole::try_from(role).unwrap_or(HostedRole::Unspecified) {
HostedRole::Reader => "reader".into(),
HostedRole::Developer => "developer".into(),
HostedRole::Maintainer => "maintainer".into(),
HostedRole::Admin => "admin".into(),
HostedRole::Owner => "owner".into(),
HostedRole::Unspecified => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grant_repository_targets_preserve_both_reference_variants() {
use api::heddle::api::v1alpha1::{GrantTargetRef, HostedRole, grant_target_ref::Target};
for reference in [
Reference::HostedId("repo_123".to_string()),
Reference::CanonicalPath("acme/widgets".to_string()),
] {
let expected = match &reference {
Reference::HostedId(value) | Reference::CanonicalPath(value) => value.clone(),
};
let grant = HostedGrant {
subject: "principal:alice".to_string(),
role: HostedRole::Developer as i32,
target: Some(GrantTargetRef {
target: Some(Target::RepoPath(RepositoryRef {
reference: Some(reference),
})),
}),
};
let mapped = to_protocol_grant(grant);
assert_eq!(mapped.repo_path.as_deref(), Some(expected.as_str()));
assert_eq!(mapped.namespace_path, None);
}
}
#[test]
fn native_auth_failure_maps_without_transport_status_types() {
let error = hosted_to_protocol_error(HostedError::Call {
code: api::heddle::api::v1alpha1::CallFailureCode::Unauthenticated,
message: "invalid proof".to_string(),
error: None,
});
assert!(matches!(error, ProtocolError::AuthorizationFailed(_)));
}
#[test]
fn native_call_failure_preserves_typed_error_detail() {
use api::heddle::api::v1alpha1::{ConflictDetail, ErrorDetail, ErrorReason, error_detail};
let error = hosted_to_protocol_error(HostedError::Call {
code: api::heddle::api::v1alpha1::CallFailureCode::AlreadyExists,
message: "ref changed".to_string(),
error: Some(Box::new(ErrorDetail {
reason: ErrorReason::VersionConflict as i32,
resource: "refs/heads/main".to_string(),
field: String::new(),
context: Some(error_detail::Context::Conflict(ConflictDetail {
resource: "refs/heads/main".to_string(),
expected_version: "old".to_string(),
actual_version: "new".to_string(),
})),
})),
});
let ProtocolError::RemoteFailure { code, details, .. } = error else {
panic!("expected remote failure");
};
assert_eq!(code, wire::RemoteFailureCode::AlreadyExists);
assert!(matches!(
&details[0],
wire::RemoteFailureDetail::Conflict { resource, .. }
if resource == "refs/heads/main"
));
}
}