Files
maka/native/gitoxide-helper/src/main.rs
T
agent-radar 7be535059e
Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 19:05:14 +08:00

2494 lines
84 KiB
Rust

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use std::{
collections::HashSet,
fs,
io::{self, Read},
path::{Path, PathBuf},
process::ExitCode,
};
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
use caseless::Caseless;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use unicode_normalization::UnicodeNormalization;
const PROTOCOL_VERSION: u8 = 1;
const MANAGED_TREE_POLICY_VERSION: u8 = 3;
const MAX_ENCODED_CONTENT_BYTES: u64 = MAX_IMPORT_FILE_BYTES.div_ceil(3) * 4;
const MAX_REQUEST_BYTES: u64 = MAX_ENCODED_CONTENT_BYTES + 64 * 1024;
const MAX_REPOSITORY_METADATA_BYTES: u64 = 1024 * 1024;
const MAX_REPOSITORY_METADATA_ENTRIES: u64 = 16_384;
const MAX_REPOSITORY_METADATA_DEPTH: u64 = 64;
const MAX_REPOSITORY_PACK_DIRECTORY_ENTRIES: u64 = 1024;
const MAX_GITOXIDE_OBJECT_STORE_SLOTS: u16 = 1024;
const MAX_IMPORT_FILE_BYTES: u64 = 64 * 1024 * 1024;
const MAX_ATTRIBUTES_FILE_BYTES: u64 = 64 * 1024;
const MAX_GIT_ATTRIBUTES_LINE_BYTES_V2: usize = 2048;
const MAX_TREE_FILE_BYTES: u64 = 8 * 1024 * 1024;
const MAX_IMPORT_BYTES: u64 = 2 * 1024 * 1024 * 1024;
const MAX_IMPORT_FILES: u64 = 200_000;
const MAX_COMMIT_OBJECT_BYTES: u64 = 1024 * 1024;
const MAX_SINGLE_TREE_OBJECT_BYTES: u64 = 8 * 1024 * 1024;
const MAX_TOTAL_TREE_OBJECT_BYTES: u64 = 64 * 1024 * 1024;
const MAX_GITOXIDE_OBJECT_ALLOCATION_BYTES: &str = "gitoxide.objects.allocLimit=67108864";
const MANAGED_TREE_POLICY_V3: ManagedTreePolicy = ManagedTreePolicy {
max_depth: 64,
max_tree_visits: 250_000,
max_entries: 400_000,
max_total_path_bytes: 256 * 1024 * 1024,
max_total_folded_path_bytes: 256 * 1024 * 1024,
max_component_bytes: 255,
max_relative_path_bytes: 4096,
max_folded_relative_path_bytes: 4096,
max_files: MAX_IMPORT_FILES,
max_file_bytes: MAX_IMPORT_FILE_BYTES,
max_bytes: MAX_IMPORT_BYTES,
max_commit_object_bytes: MAX_COMMIT_OBJECT_BYTES,
max_single_tree_object_bytes: MAX_SINGLE_TREE_OBJECT_BYTES,
max_total_tree_object_bytes: MAX_TOTAL_TREE_OBJECT_BYTES,
};
const HELPER_ERROR_REASONS_V1: &[&str] = &[
"internal_error_reason_invalid",
"request_read_failed",
"request_too_large",
"invalid_request",
"unsupported_protocol_version",
"repository_metadata_limit_exceeded",
"repository_alternates_unsupported",
"repository_open_failed",
"head_commit_unavailable",
"head_commit_identity_mismatch",
"head_tree_unavailable",
"commit_object_limit_exceeded",
"baseline_commit_write_failed",
"baseline_publish_failed",
"baseline_ref_outside_maka_namespace",
"base_commit_unavailable",
"base_commit_identity_mismatch",
"base_path_lookup_failed",
"base_tree_unavailable",
"base_tree_identity_mismatch",
"blob_write_failed",
"commit_write_failed",
"invalid_baseline_ref",
"invalid_base_commit_oid",
"invalid_successor_path",
"import_destination_create_failed",
"import_destination_not_fresh",
"import_destination_object_format_mismatch",
"import_destination_parent_untrusted",
"invalid_source_head_commit_oid",
"source_blob_copy_failed",
"source_blob_identity_mismatch",
"source_blob_invalid",
"source_blob_unavailable",
"source_attributes_limit_exceeded",
"source_byte_limit_exceeded",
"source_file_limit_exceeded",
"source_folded_path_byte_limit_exceeded",
"source_folded_path_length_exceeded",
"source_head_commit_mismatch",
"source_head_commit_identity_mismatch",
"source_head_commit_unavailable",
"source_head_tree_unavailable",
"source_path_collision",
"source_path_byte_limit_exceeded",
"source_path_length_exceeded",
"source_tree_copy_failed",
"source_tree_depth_exceeded",
"source_tree_entry_limit_exceeded",
"source_tree_identity_mismatch",
"source_tree_invalid",
"source_tree_object_byte_limit_exceeded",
"source_tree_object_limit_exceeded",
"source_tree_noncanonical_mode",
"source_tree_not_sorted",
"source_tree_observation_mismatch",
"source_tree_unavailable",
"source_tree_visit_limit_exceeded",
"successor_content_limit_exceeded",
"successor_commit_identity_mismatch",
"accepted_ref_not_direct",
"accepted_ref_target_invalid",
"candidate_ref_not_direct",
"candidate_ref_target_invalid",
"candidate_request_conflict",
"candidate_publication_indeterminate",
"target_ref_outside_maka_namespace",
"target_ref_unavailable",
"tree_edit_failed",
"tree_write_failed",
"unsupported_base_path_kind",
"accepted_commit_unavailable",
"accepted_tree_unavailable",
"invalid_accepted_commit_oid",
"invalid_tree_file_path",
"tree_file_identity_mismatch",
"tree_file_invalid",
"tree_file_lookup_failed",
"tree_file_not_utf8",
"tree_file_size_limit_exceeded",
"tree_file_unavailable",
"unsupported_source_entry_kind",
"unsupported_source_attributes",
"unsupported_source_path",
"unsupported_object_format",
"unsupported_managed_tree_policy",
];
#[derive(Deserialize)]
#[serde(
deny_unknown_fields,
tag = "operation",
rename_all = "snake_case",
rename_all_fields = "camelCase"
)]
enum Request {
InspectRepository {
protocol_version: u8,
repository_path: PathBuf,
},
ImportSourceHead {
protocol_version: u8,
source_repository_path: PathBuf,
expected_source_head_commit_oid: String,
destination_repository_path: PathBuf,
baseline_ref: String,
managed_tree_policy_version: u8,
},
CreateCandidate {
protocol_version: u8,
repository_path: PathBuf,
accepted_ref: String,
expected_base_commit_oid: String,
expected_base_tree_oid: String,
candidate_ref: String,
path: String,
content_base64: String,
managed_tree_policy_version: u8,
},
ReadTreeFile {
protocol_version: u8,
repository_path: PathBuf,
accepted_commit_oid: String,
path: String,
managed_tree_policy_version: u8,
},
}
#[derive(Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
enum Response<'a> {
#[serde(rename_all = "camelCase")]
RepositoryInspected {
protocol_version: u8,
object_format: &'static str,
head_commit_oid: String,
head_tree_oid: String,
},
#[serde(rename_all = "camelCase")]
RepositoryRejected {
protocol_version: u8,
reason: &'static str,
object_format: String,
supported_object_formats: [&'static str; 1],
},
#[serde(rename_all = "camelCase")]
SourceImported {
protocol_version: u8,
object_format: &'static str,
source_head_commit_oid: String,
source_tree_oid: String,
baseline_commit_oid: String,
baseline_tree_oid: String,
baseline_ref: String,
managed_tree_policy_version: u8,
files_imported: u64,
bytes_imported: u64,
},
#[serde(rename_all = "camelCase")]
CandidatePublished {
protocol_version: u8,
object_format: &'static str,
base_commit_oid: String,
base_tree_oid: String,
candidate_commit_oid: String,
candidate_tree_oid: String,
result_blob_oid: String,
request_digest_sha256: String,
accepted_ref: String,
candidate_ref: String,
path: String,
managed_tree_policy_version: u8,
},
#[serde(rename_all = "camelCase")]
CandidateNoChange {
protocol_version: u8,
object_format: &'static str,
base_commit_oid: String,
base_tree_oid: String,
result_blob_oid: String,
candidate_commit_oid: String,
candidate_tree_oid: String,
request_digest_sha256: String,
accepted_ref: String,
candidate_ref: String,
path: String,
managed_tree_policy_version: u8,
},
#[serde(rename_all = "camelCase")]
CandidateRejected {
protocol_version: u8,
reason: &'static str,
object_format: &'static str,
expected_base_commit_oid: String,
actual_base_commit_oid: String,
accepted_ref: String,
candidate_ref: String,
managed_tree_policy_version: u8,
},
#[serde(rename_all = "camelCase")]
TreeFileRead {
protocol_version: u8,
object_format: &'static str,
accepted_commit_oid: String,
accepted_tree_oid: String,
blob_oid: String,
path: String,
content: String,
bytes_read: u64,
managed_tree_policy_version: u8,
},
#[serde(rename_all = "camelCase")]
HelperError {
protocol_version: u8,
reason: &'a str,
},
}
fn main() -> ExitCode {
match run() {
Ok(code) => code,
Err(reason) => {
let reason = if HELPER_ERROR_REASONS_V1.contains(&reason) {
reason
} else {
"internal_error_reason_invalid"
};
write_response(&Response::HelperError {
protocol_version: PROTOCOL_VERSION,
reason,
});
ExitCode::from(1)
}
}
}
fn run() -> Result<ExitCode, &'static str> {
let request = read_request()?;
match request {
Request::InspectRepository {
protocol_version,
repository_path,
} => {
assert_protocol_version(protocol_version)?;
inspect_repository(repository_path)
}
Request::ImportSourceHead {
protocol_version,
source_repository_path,
expected_source_head_commit_oid,
destination_repository_path,
baseline_ref,
managed_tree_policy_version,
} => {
assert_protocol_version(protocol_version)?;
import_source_head(
source_repository_path,
expected_source_head_commit_oid,
destination_repository_path,
baseline_ref,
managed_tree_policy_version,
)
}
Request::CreateCandidate {
protocol_version,
repository_path,
accepted_ref,
expected_base_commit_oid,
expected_base_tree_oid,
candidate_ref,
path,
content_base64,
managed_tree_policy_version,
} => {
assert_protocol_version(protocol_version)?;
create_candidate(
repository_path,
accepted_ref,
expected_base_commit_oid,
expected_base_tree_oid,
candidate_ref,
path,
content_base64,
managed_tree_policy_version,
)
}
Request::ReadTreeFile {
protocol_version,
repository_path,
accepted_commit_oid,
path,
managed_tree_policy_version,
} => {
assert_protocol_version(protocol_version)?;
read_tree_file(
repository_path,
accepted_commit_oid,
path,
managed_tree_policy_version,
)
}
}
}
fn assert_protocol_version(protocol_version: u8) -> Result<(), &'static str> {
if protocol_version != PROTOCOL_VERSION {
return Err("unsupported_protocol_version");
}
Ok(())
}
fn inspect_repository(repository_path: PathBuf) -> Result<ExitCode, &'static str> {
let mut metadata_budget = admit_repository_metadata(&repository_path)?;
let repository = match managed_open_options().open(repository_path) {
Ok(repository) => repository.to_thread_local(),
Err(gix::open::Error::Config(gix::config::Error::ConfigTypedString(error)))
if error.key.as_slice() == b"extensions.objectFormat" =>
{
return Ok(reject_unsupported_object_format("unknown".to_owned()));
}
Err(gix::open::Error::Config(gix::config::Error::UnsupportedObjectFormat { .. })) => {
return Ok(reject_unsupported_object_format("unknown".to_owned()));
}
Err(_) => return Err("repository_open_failed"),
};
metadata_budget.observe_opened_repository(&repository)?;
match repository.object_hash() {
gix::hash::Kind::Sha1 => {
let head_id = repository
.head_id()
.map_err(|_| "head_commit_unavailable")?
.detach();
let head = load_verified_object(
&repository,
head_id,
gix::objs::Kind::Commit,
MAX_COMMIT_OBJECT_BYTES,
"head_commit_unavailable",
"head_commit_unavailable",
"commit_object_limit_exceeded",
"head_commit_identity_mismatch",
)?
.try_into_commit()
.map_err(|_| "head_commit_unavailable")?;
let head_commit_oid = head.id().detach().to_string();
let head_tree_oid = head
.tree_id()
.map_err(|_| "head_tree_unavailable")?
.detach()
.to_string();
write_response(&Response::RepositoryInspected {
protocol_version: PROTOCOL_VERSION,
object_format: "sha1",
head_commit_oid,
head_tree_oid,
});
Ok(ExitCode::SUCCESS)
}
gix::hash::Kind::Sha256 => Ok(reject_unsupported_object_format("sha256".to_owned())),
_ => Ok(reject_unsupported_object_format("unknown".to_owned())),
}
}
fn open_repository(repository_path: PathBuf) -> Result<gix::Repository, &'static str> {
let mut metadata_budget = admit_repository_metadata(&repository_path)?;
let repository = managed_open_options()
.open(repository_path)
.map_err(|_| "repository_open_failed")?
.to_thread_local();
metadata_budget.observe_opened_repository(&repository)?;
Ok(repository)
}
fn managed_open_options() -> gix::open::Options {
gix::open::Options::isolated()
.lossy_config(true)
.strict_config(true)
.object_store_slots(gix::odb::store::init::Slots::Given(
MAX_GITOXIDE_OBJECT_STORE_SLOTS,
))
.config_overrides([MAX_GITOXIDE_OBJECT_ALLOCATION_BYTES])
}
fn admit_repository_metadata(
repository_path: &Path,
) -> Result<RepositoryMetadataBudget, &'static str> {
let mut budget = RepositoryMetadataBudget::default();
if repository_path
.file_name()
.is_none_or(|name| name != ".git")
{
let worktree_candidate = repository_path.join(".git");
budget.observe_repository_candidate(&worktree_candidate)?;
if gix::discover::is_git(&worktree_candidate).is_ok() {
return Ok(budget);
}
}
budget.observe_repository_candidate(repository_path)?;
Ok(budget)
}
#[derive(Default)]
struct RepositoryMetadataBudget {
observed_bytes: u64,
observed_entries: u64,
observed_paths: HashSet<PathBuf>,
}
impl RepositoryMetadataBudget {
fn observe_repository_candidate(&mut self, candidate: &Path) -> Result<(), &'static str> {
match fs::symlink_metadata(candidate) {
Ok(metadata) if metadata.file_type().is_symlink() => Err("repository_open_failed"),
Ok(metadata) if metadata.is_file() => {
self.observe_file_with_metadata(candidate, &metadata)?;
let git_dir = gix::discover::path::from_gitdir_file(candidate)
.map_err(|_| "repository_open_failed")?;
self.observe_git_dir(&git_dir)
}
Ok(metadata) if metadata.is_dir() => self.observe_git_dir(candidate),
Ok(_) => Err("repository_open_failed"),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
Err(_) => Err("repository_open_failed"),
}
}
fn observe_opened_repository(
&mut self,
repository: &gix::Repository,
) -> Result<(), &'static str> {
self.observe_git_dir(repository.git_dir())?;
self.observe_common_dir(repository.common_dir())
}
fn observe_git_dir(&mut self, git_dir: &Path) -> Result<(), &'static str> {
self.observe_optional_file(&git_dir.join("HEAD"))?;
self.observe_optional_file(&git_dir.join("gitdir"))?;
self.observe_optional_file(&git_dir.join("config.worktree"))?;
self.observe_optional_file(&git_dir.join("packed-refs"))?;
self.observe_optional_file(&git_dir.join("shallow"))?;
self.observe_tree(&git_dir.join("refs"), 0)?;
let common_dir_path_file = git_dir.join("commondir");
self.observe_optional_file(&common_dir_path_file)?;
let common_dir = match gix::discover::path::from_plain_file(&common_dir_path_file) {
Some(Ok(relative_or_absolute)) => git_dir.join(relative_or_absolute),
Some(Err(_)) => return Err("repository_open_failed"),
None => git_dir.to_path_buf(),
};
self.observe_common_dir(&common_dir)
}
fn observe_common_dir(&mut self, common_dir: &Path) -> Result<(), &'static str> {
self.observe_optional_file(&common_dir.join("config"))?;
self.observe_optional_file(&common_dir.join("packed-refs"))?;
self.observe_optional_file(&common_dir.join("shallow"))?;
self.reject_alternates(&common_dir.join("objects/info/alternates"))?;
self.reject_alternates(&common_dir.join("objects/info/http-alternates"))?;
self.observe_pack_directory(&common_dir.join("objects/pack"))?;
self.observe_tree(&common_dir.join("refs"), 0)
}
fn reject_alternates(&self, path: &Path) -> Result<(), &'static str> {
match fs::symlink_metadata(path) {
Ok(_) => Err("repository_alternates_unsupported"),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
Err(_) => Err("repository_open_failed"),
}
}
fn observe_pack_directory(&mut self, path: &Path) -> Result<(), &'static str> {
match fs::symlink_metadata(path) {
Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_dir() => {
return Err("repository_open_failed");
}
Ok(_) => self.observe_entry(path, 0)?,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(_) => return Err("repository_open_failed"),
}
let entries = fs::read_dir(path).map_err(|_| "repository_open_failed")?;
let mut pack_entries = 0u64;
for entry in entries {
let entry = entry.map_err(|_| "repository_open_failed")?;
pack_entries = pack_entries
.checked_add(1)
.filter(|entries| *entries <= MAX_REPOSITORY_PACK_DIRECTORY_ENTRIES)
.ok_or("repository_metadata_limit_exceeded")?;
let entry_path = entry.path();
let metadata =
fs::symlink_metadata(&entry_path).map_err(|_| "repository_open_failed")?;
if metadata.file_type().is_symlink() || !metadata.is_file() {
return Err("repository_open_failed");
}
let path_bytes = u64::try_from(entry.file_name().as_encoded_bytes().len())
.map_err(|_| "repository_metadata_limit_exceeded")?;
self.observe_entry(&entry_path, path_bytes)?;
}
Ok(())
}
fn observe_tree(&mut self, path: &Path, depth: u64) -> Result<(), &'static str> {
if depth > MAX_REPOSITORY_METADATA_DEPTH {
return Err("repository_metadata_limit_exceeded");
}
let entries = match fs::read_dir(path) {
Ok(entries) => entries,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(_) => return Err("repository_open_failed"),
};
for entry in entries {
let entry = entry.map_err(|_| "repository_open_failed")?;
let entry_path = entry.path();
let metadata =
fs::symlink_metadata(&entry_path).map_err(|_| "repository_open_failed")?;
if metadata.file_type().is_symlink() {
return Err("repository_open_failed");
}
if metadata.is_dir() {
self.observe_entry(&entry_path, 0)?;
self.observe_tree(&entry_path, depth + 1)?;
} else if metadata.is_file() {
self.observe_file_with_metadata(&entry_path, &metadata)?;
} else {
return Err("repository_open_failed");
}
}
Ok(())
}
fn observe_optional_file(&mut self, path: &Path) -> Result<(), &'static str> {
match fs::symlink_metadata(path) {
Ok(metadata) if metadata.file_type().is_symlink() => Err("repository_open_failed"),
Ok(metadata) if metadata.is_file() => self.observe_file_with_metadata(path, &metadata),
Ok(_) => Err("repository_open_failed"),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
Err(_) => Err("repository_open_failed"),
}
}
fn observe_file_with_metadata(
&mut self,
path: &Path,
metadata: &fs::Metadata,
) -> Result<(), &'static str> {
self.observe_entry(path, metadata.len())
}
fn observe_entry(&mut self, path: &Path, bytes: u64) -> Result<(), &'static str> {
if !self.observed_paths.insert(path.to_path_buf()) {
return Ok(());
}
self.observed_entries = self
.observed_entries
.checked_add(1)
.filter(|entries| *entries <= MAX_REPOSITORY_METADATA_ENTRIES)
.ok_or("repository_metadata_limit_exceeded")?;
self.observed_bytes = self
.observed_bytes
.checked_add(bytes)
.filter(|observed| *observed <= MAX_REPOSITORY_METADATA_BYTES)
.ok_or("repository_metadata_limit_exceeded")?;
Ok(())
}
}
fn import_source_head(
source_repository_path: PathBuf,
expected_source_head_commit_oid: String,
destination_repository_path: PathBuf,
baseline_ref: String,
managed_tree_policy_version: u8,
) -> Result<ExitCode, &'static str> {
use gix::bstr::ByteSlice;
if !baseline_ref.starts_with("refs/maka/") {
return Err("baseline_ref_outside_maka_namespace");
}
gix::refs::FullName::try_from(baseline_ref.as_str()).map_err(|_| "invalid_baseline_ref")?;
if managed_tree_policy_version != MANAGED_TREE_POLICY_VERSION {
return Err("unsupported_managed_tree_policy");
}
let source = open_repository(source_repository_path)?;
if source.object_hash() != gix::hash::Kind::Sha1 {
return Err("unsupported_object_format");
}
let expected_source_head =
gix::hash::ObjectId::from_hex(expected_source_head_commit_oid.as_bytes())
.map_err(|_| "invalid_source_head_commit_oid")?;
if expected_source_head.kind() != gix::hash::Kind::Sha1 {
return Err("invalid_source_head_commit_oid");
}
let source_head_id = source
.head_id()
.map_err(|_| "source_head_commit_unavailable")?;
if source_head_id.detach() != expected_source_head {
return Err("source_head_commit_mismatch");
}
let source_head = load_verified_object(
&source,
expected_source_head,
gix::objs::Kind::Commit,
MANAGED_TREE_POLICY_V3.max_commit_object_bytes,
"source_head_commit_unavailable",
"source_head_commit_unavailable",
"commit_object_limit_exceeded",
"source_head_commit_identity_mismatch",
)?
.try_into_commit()
.map_err(|_| "source_head_commit_unavailable")?;
let source_tree = source_head
.tree_id()
.map_err(|_| "source_head_tree_unavailable")?
.detach();
let mut stats = ManagedTreeStats::default();
walk_verified_source_tree(
&source,
None,
source_tree,
"",
0,
MANAGED_TREE_POLICY_V3,
&mut stats,
)?;
let expected_files = stats.files;
let expected_bytes = stats.bytes;
drop(stats);
assert_import_destination_parent(&destination_repository_path)?;
let destination = claim_fresh_import_destination(&destination_repository_path)?;
if destination.object_hash() != gix::hash::Kind::Sha1 {
return Err("import_destination_object_format_mismatch");
}
let mut copy_stats = ManagedTreeStats::default();
walk_verified_source_tree(
&source,
Some(&destination),
source_tree,
"",
0,
MANAGED_TREE_POLICY_V3,
&mut copy_stats,
)?;
if copy_stats.files != expected_files || copy_stats.bytes != expected_bytes {
return Err("source_tree_observation_mismatch");
}
let signature = gix::actor::SignatureRef {
name: b"Maka Workspace Service".as_bstr(),
email: b"workspace@maka.invalid".as_bstr(),
time: "946684800 +0000",
};
let baseline_commit = destination
.new_commit_as(
signature,
signature,
"maka managed workspace baseline v2",
source_tree,
std::iter::empty::<gix::hash::ObjectId>(),
)
.map_err(|_| "baseline_commit_write_failed")?
.id()
.detach();
destination
.reference(
baseline_ref.as_str(),
baseline_commit,
gix::refs::transaction::PreviousValue::MustNotExist,
"maka managed workspace baseline",
)
.map_err(|_| "baseline_publish_failed")?;
write_response(&Response::SourceImported {
protocol_version: PROTOCOL_VERSION,
object_format: "sha1",
source_head_commit_oid: expected_source_head.to_string(),
source_tree_oid: source_tree.to_string(),
baseline_commit_oid: baseline_commit.to_string(),
baseline_tree_oid: source_tree.to_string(),
baseline_ref,
managed_tree_policy_version: MANAGED_TREE_POLICY_VERSION,
files_imported: copy_stats.files,
bytes_imported: copy_stats.bytes,
});
Ok(ExitCode::SUCCESS)
}
fn create_candidate(
repository_path: PathBuf,
accepted_ref: String,
expected_base_commit_oid: String,
expected_base_tree_oid: String,
candidate_ref: String,
path: String,
content_base64: String,
managed_tree_policy_version: u8,
) -> Result<ExitCode, &'static str> {
use gix::bstr::ByteSlice;
if !accepted_ref.starts_with("refs/maka/") {
return Err("target_ref_outside_maka_namespace");
}
gix::refs::FullName::try_from(accepted_ref.as_str())
.map_err(|_| "target_ref_outside_maka_namespace")?;
if !candidate_ref.starts_with("refs/maka/candidates/") {
return Err("target_ref_outside_maka_namespace");
}
gix::refs::FullName::try_from(candidate_ref.as_str())
.map_err(|_| "target_ref_outside_maka_namespace")?;
if managed_tree_policy_version != MANAGED_TREE_POLICY_VERSION {
return Err("unsupported_managed_tree_policy");
}
if !is_canonical_successor_path_v3(&path) {
return Err("invalid_successor_path");
}
if content_base64.len() as u64 > MAX_ENCODED_CONTENT_BYTES {
return Err("successor_content_limit_exceeded");
}
let content = BASE64_STANDARD
.decode(content_base64)
.map_err(|_| "invalid_request")?;
if content.len() as u64 > MAX_IMPORT_FILE_BYTES {
return Err("successor_content_limit_exceeded");
}
std::str::from_utf8(&content).map_err(|_| "invalid_request")?;
let request_digest_sha256 = candidate_request_digest_sha256(
&accepted_ref,
&expected_base_commit_oid,
&expected_base_tree_oid,
&candidate_ref,
&path,
&content,
);
let repository = open_repository(repository_path)?;
if repository.object_hash() != gix::hash::Kind::Sha1 {
return Err("unsupported_object_format");
}
let expected_base = gix::hash::ObjectId::from_hex(expected_base_commit_oid.as_bytes())
.map_err(|_| "invalid_base_commit_oid")?;
let expected_base_tree = gix::hash::ObjectId::from_hex(expected_base_tree_oid.as_bytes())
.map_err(|_| "invalid_base_commit_oid")?;
if expected_base.kind() != gix::hash::Kind::Sha1 {
return Err("invalid_base_commit_oid");
}
let base_commit = load_verified_object(
&repository,
expected_base,
gix::objs::Kind::Commit,
MANAGED_TREE_POLICY_V3.max_commit_object_bytes,
"base_commit_unavailable",
"base_commit_unavailable",
"commit_object_limit_exceeded",
"base_commit_identity_mismatch",
)?
.try_into_commit()
.map_err(|_| "base_commit_unavailable")?;
let base_tree = base_commit
.tree_id()
.map_err(|_| "base_tree_unavailable")?
.detach();
if base_tree != expected_base_tree {
return Err("base_tree_unavailable");
}
load_verified_object(
&repository,
expected_base_tree,
gix::objs::Kind::Tree,
MANAGED_TREE_POLICY_V3.max_single_tree_object_bytes,
"base_tree_unavailable",
"base_tree_unavailable",
"source_tree_object_limit_exceeded",
"base_tree_identity_mismatch",
)?;
let current = read_direct_commit_ref(
&repository,
&accepted_ref,
"accepted_ref_not_direct",
"accepted_ref_target_invalid",
)?;
if current != expected_base {
write_response(&Response::CandidateRejected {
protocol_version: PROTOCOL_VERSION,
reason: "base_commit_mismatch",
object_format: "sha1",
expected_base_commit_oid: expected_base.to_string(),
actual_base_commit_oid: current.to_string(),
accepted_ref,
candidate_ref,
managed_tree_policy_version: MANAGED_TREE_POLICY_VERSION,
});
return Ok(ExitCode::from(3));
}
let result_blob =
gix::objs::compute_hash(repository.object_hash(), gix::objs::Kind::Blob, &content)
.map_err(|_| "blob_write_failed")?;
let existing_entry = lookup_verified_tree_entry(&repository, expected_base_tree, &path)?;
let entry_kind = match existing_entry.as_ref().map(|(kind, _)| *kind) {
Some(gix::objs::tree::EntryKind::BlobExecutable) => {
gix::objs::tree::EntryKind::BlobExecutable
}
Some(gix::objs::tree::EntryKind::Blob) | None => gix::objs::tree::EntryKind::Blob,
Some(_) => return Err("unsupported_base_path_kind"),
};
let no_change = existing_entry
.as_ref()
.is_some_and(|(_, oid)| *oid == result_blob);
if let Some(reference) = repository
.try_find_reference(candidate_ref.as_str())
.map_err(|_| "target_ref_unavailable")?
{
let existing = read_direct_commit_reference(
&repository,
reference,
"candidate_ref_not_direct",
"candidate_ref_target_invalid",
)?;
let existing_tree = verify_existing_candidate_receipt(
&repository,
existing,
expected_base,
expected_base_tree,
&path,
entry_kind,
result_blob,
&request_digest_sha256,
)?;
let mut retry_stats = ManagedTreeStats::default();
walk_verified_source_tree(
&repository,
None,
existing_tree,
"",
0,
MANAGED_TREE_POLICY_V3,
&mut retry_stats,
)?;
write_candidate_response(
existing_tree == expected_base_tree,
expected_base,
expected_base_tree,
existing,
existing_tree,
result_blob,
&request_digest_sha256,
&accepted_ref,
&candidate_ref,
&path,
);
return Ok(ExitCode::SUCCESS);
}
let mut base_stats = ManagedTreeStats::default();
walk_verified_source_tree(
&repository,
None,
expected_base_tree,
"",
0,
MANAGED_TREE_POLICY_V3,
&mut base_stats,
)?;
drop(base_stats);
let successor_tree = if no_change {
expected_base_tree
} else {
let written_blob = repository
.write_blob(&content)
.map_err(|_| "blob_write_failed")?
.detach();
if written_blob != result_blob {
return Err("blob_write_failed");
}
write_candidate_tree_from_verified_base(
&repository,
expected_base_tree,
&path,
entry_kind,
result_blob,
)?
};
let mut stats = ManagedTreeStats::default();
walk_verified_source_tree(
&repository,
None,
successor_tree,
"",
0,
MANAGED_TREE_POLICY_V3,
&mut stats,
)?;
let signature = gix::actor::SignatureRef {
name: b"Maka Workspace Service".as_bstr(),
email: b"workspace@maka.invalid".as_bstr(),
time: "946684800 +0000",
};
let commit_message =
format!("maka managed workspace candidate v3\nrequest-sha256 {request_digest_sha256}");
let successor_commit = repository
.new_commit_as(
signature,
signature,
commit_message,
successor_tree,
[expected_base],
)
.map_err(|_| "commit_write_failed")?
.id()
.detach();
load_verified_object(
&repository,
successor_commit,
gix::objs::Kind::Commit,
MANAGED_TREE_POLICY_V3.max_commit_object_bytes,
"commit_write_failed",
"commit_write_failed",
"commit_object_limit_exceeded",
"successor_commit_identity_mismatch",
)?;
verify_candidate_commit_and_result(
&repository,
successor_commit,
expected_base,
successor_tree,
&path,
result_blob,
&request_digest_sha256,
)?;
let accepted_current = read_direct_commit_ref(
&repository,
&accepted_ref,
"accepted_ref_not_direct",
"accepted_ref_target_invalid",
)?;
if accepted_current != expected_base {
write_response(&Response::CandidateRejected {
protocol_version: PROTOCOL_VERSION,
reason: "base_commit_mismatch",
object_format: "sha1",
expected_base_commit_oid: expected_base.to_string(),
actual_base_commit_oid: accepted_current.to_string(),
accepted_ref,
candidate_ref,
managed_tree_policy_version: MANAGED_TREE_POLICY_VERSION,
});
return Ok(ExitCode::from(3));
}
match repository
.try_find_reference(candidate_ref.as_str())
.map_err(|_| "target_ref_unavailable")?
{
Some(reference) => {
let existing = read_direct_commit_reference(
&repository,
reference,
"candidate_ref_not_direct",
"candidate_ref_target_invalid",
)?;
if existing != successor_commit {
return Err("candidate_request_conflict");
}
}
None => match repository.reference(
candidate_ref.as_str(),
successor_commit,
gix::refs::transaction::PreviousValue::MustNotExist,
"maka managed workspace candidate",
) {
Ok(_) => {}
Err(_) => {
let concurrent = repository
.try_find_reference(candidate_ref.as_str())
.map_err(|_| "candidate_publication_indeterminate")?
.ok_or("candidate_publication_indeterminate")?;
let concurrent = read_direct_commit_reference(
&repository,
concurrent,
"candidate_ref_not_direct",
"candidate_ref_target_invalid",
)?;
if concurrent != successor_commit {
return Err("candidate_request_conflict");
}
}
},
}
write_candidate_response(
no_change,
expected_base,
expected_base_tree,
successor_commit,
successor_tree,
result_blob,
&request_digest_sha256,
&accepted_ref,
&candidate_ref,
&path,
);
Ok(ExitCode::SUCCESS)
}
fn verify_existing_candidate_receipt(
repository: &gix::Repository,
candidate_commit_oid: gix::hash::ObjectId,
expected_base_commit_oid: gix::hash::ObjectId,
expected_base_tree_oid: gix::hash::ObjectId,
path: &str,
entry_kind: gix::objs::tree::EntryKind,
expected_result_blob_oid: gix::hash::ObjectId,
request_digest_sha256: &str,
) -> Result<gix::hash::ObjectId, &'static str> {
let commit = load_verified_object(
repository,
candidate_commit_oid,
gix::objs::Kind::Commit,
MANAGED_TREE_POLICY_V3.max_commit_object_bytes,
"candidate_ref_target_invalid",
"candidate_ref_target_invalid",
"commit_object_limit_exceeded",
"candidate_ref_target_invalid",
)?
.try_into_commit()
.map_err(|_| "candidate_ref_target_invalid")?
.decode()
.map_err(|_| "candidate_ref_target_invalid")?
.into_owned()
.map_err(|_| "candidate_ref_target_invalid")?;
let expected_message =
format!("maka managed workspace candidate v3\nrequest-sha256 {request_digest_sha256}");
if commit.message.as_slice() != expected_message.as_bytes() {
return Err("candidate_request_conflict");
}
if commit.parents.as_slice() != [expected_base_commit_oid]
|| !has_managed_candidate_signature(&commit)
{
return Err("candidate_ref_target_invalid");
}
let candidate_tree = commit.tree;
let expected_candidate_tree = compute_candidate_tree_from_verified_base(
repository,
expected_base_tree_oid,
path,
entry_kind,
expected_result_blob_oid,
)?;
if candidate_tree != expected_candidate_tree {
return Err("candidate_ref_target_invalid");
}
let result_entry = lookup_verified_tree_entry(repository, candidate_tree, path)?
.ok_or("candidate_ref_target_invalid")?;
if result_entry.1 != expected_result_blob_oid {
return Err("candidate_ref_target_invalid");
}
load_verified_object(
repository,
expected_result_blob_oid,
gix::objs::Kind::Blob,
MAX_IMPORT_FILE_BYTES,
"candidate_ref_target_invalid",
"candidate_ref_target_invalid",
"candidate_ref_target_invalid",
"candidate_ref_target_invalid",
)?;
Ok(candidate_tree)
}
fn has_managed_candidate_signature(commit: &gix::objs::Commit) -> bool {
let expected_name = b"Maka Workspace Service";
let expected_email = b"workspace@maka.invalid";
commit.author.name.as_slice() == expected_name.as_slice()
&& commit.author.email.as_slice() == expected_email.as_slice()
&& commit.author.time.seconds == 946_684_800
&& commit.author.time.offset == 0
&& commit.committer.name.as_slice() == expected_name.as_slice()
&& commit.committer.email.as_slice() == expected_email.as_slice()
&& commit.committer.time.seconds == 946_684_800
&& commit.committer.time.offset == 0
&& commit.encoding.is_none()
&& commit.extra_headers.is_empty()
}
#[allow(clippy::too_many_arguments)]
fn write_candidate_response(
no_change: bool,
base_commit_oid: gix::hash::ObjectId,
base_tree_oid: gix::hash::ObjectId,
candidate_commit_oid: gix::hash::ObjectId,
candidate_tree_oid: gix::hash::ObjectId,
result_blob_oid: gix::hash::ObjectId,
request_digest_sha256: &str,
accepted_ref: &str,
candidate_ref: &str,
path: &str,
) {
if no_change {
write_response(&Response::CandidateNoChange {
protocol_version: PROTOCOL_VERSION,
object_format: "sha1",
base_commit_oid: base_commit_oid.to_string(),
base_tree_oid: base_tree_oid.to_string(),
result_blob_oid: result_blob_oid.to_string(),
candidate_commit_oid: candidate_commit_oid.to_string(),
candidate_tree_oid: candidate_tree_oid.to_string(),
request_digest_sha256: request_digest_sha256.to_owned(),
accepted_ref: accepted_ref.to_owned(),
candidate_ref: candidate_ref.to_owned(),
path: path.to_owned(),
managed_tree_policy_version: MANAGED_TREE_POLICY_VERSION,
});
} else {
write_response(&Response::CandidatePublished {
protocol_version: PROTOCOL_VERSION,
object_format: "sha1",
base_commit_oid: base_commit_oid.to_string(),
base_tree_oid: base_tree_oid.to_string(),
candidate_commit_oid: candidate_commit_oid.to_string(),
candidate_tree_oid: candidate_tree_oid.to_string(),
result_blob_oid: result_blob_oid.to_string(),
request_digest_sha256: request_digest_sha256.to_owned(),
accepted_ref: accepted_ref.to_owned(),
candidate_ref: candidate_ref.to_owned(),
path: path.to_owned(),
managed_tree_policy_version: MANAGED_TREE_POLICY_VERSION,
});
}
}
fn read_direct_commit_ref(
repository: &gix::Repository,
ref_name: &str,
not_direct_error: &'static str,
invalid_target_error: &'static str,
) -> Result<gix::hash::ObjectId, &'static str> {
let reference = repository
.find_reference(ref_name)
.map_err(|_| "target_ref_unavailable")?;
read_direct_commit_reference(
repository,
reference,
not_direct_error,
invalid_target_error,
)
}
fn read_direct_commit_reference(
repository: &gix::Repository,
reference: gix::Reference<'_>,
not_direct_error: &'static str,
invalid_target_error: &'static str,
) -> Result<gix::hash::ObjectId, &'static str> {
let target = reference.try_id().ok_or(not_direct_error)?.detach();
load_verified_object(
repository,
target,
gix::objs::Kind::Commit,
MANAGED_TREE_POLICY_V3.max_commit_object_bytes,
invalid_target_error,
invalid_target_error,
"commit_object_limit_exceeded",
invalid_target_error,
)?;
Ok(target)
}
struct VerifiedTreeFinder<'repo> {
repository: &'repo gix::Repository,
}
impl gix::objs::Find for VerifiedTreeFinder<'_> {
fn try_find<'a>(
&self,
id: &gix::hash::oid,
buffer: &'a mut Vec<u8>,
) -> Result<Option<gix::objs::Data<'a>>, gix::objs::find::Error> {
let Some(data) = gix::objs::Find::try_find(self.repository, id, buffer)? else {
return Ok(None);
};
if data.kind != gix::objs::Kind::Tree {
return Err(format!("expected tree object at {id}").into());
}
data.verify_checksum(id)?;
Ok(Some(data))
}
}
fn write_candidate_tree_from_verified_base(
repository: &gix::Repository,
base_tree_oid: gix::hash::ObjectId,
path: &str,
entry_kind: gix::objs::tree::EntryKind,
result_blob_oid: gix::hash::ObjectId,
) -> Result<gix::hash::ObjectId, &'static str> {
edit_candidate_tree_from_verified_base(
repository,
base_tree_oid,
path,
entry_kind,
result_blob_oid,
|tree| {
repository
.write_object(tree)
.map(|id| id.detach())
.map_err(|_| ())
},
)
}
fn compute_candidate_tree_from_verified_base(
repository: &gix::Repository,
base_tree_oid: gix::hash::ObjectId,
path: &str,
entry_kind: gix::objs::tree::EntryKind,
result_blob_oid: gix::hash::ObjectId,
) -> Result<gix::hash::ObjectId, &'static str> {
use gix::objs::WriteTo;
edit_candidate_tree_from_verified_base(
repository,
base_tree_oid,
path,
entry_kind,
result_blob_oid,
|tree| {
let mut encoded = Vec::new();
tree.write_to(&mut encoded).map_err(|_| ())?;
gix::objs::compute_hash(repository.object_hash(), gix::objs::Kind::Tree, &encoded)
.map_err(|_| ())
},
)
}
fn edit_candidate_tree_from_verified_base<E>(
repository: &gix::Repository,
base_tree_oid: gix::hash::ObjectId,
path: &str,
entry_kind: gix::objs::tree::EntryKind,
result_blob_oid: gix::hash::ObjectId,
write_tree: impl FnMut(&gix::objs::Tree) -> Result<gix::hash::ObjectId, E>,
) -> Result<gix::hash::ObjectId, &'static str> {
let root = load_verified_object(
repository,
base_tree_oid,
gix::objs::Kind::Tree,
MANAGED_TREE_POLICY_V3.max_single_tree_object_bytes,
"base_tree_unavailable",
"base_tree_unavailable",
"source_tree_object_limit_exceeded",
"base_tree_identity_mismatch",
)?
.try_into_tree()
.map_err(|_| "base_tree_unavailable")?
.decode()
.map_err(|_| "base_tree_unavailable")?
.into_owned();
let finder = VerifiedTreeFinder { repository };
let mut editor = gix::objs::tree::Editor::new(root, &finder, repository.object_hash());
editor
.upsert(path.split('/'), entry_kind, result_blob_oid)
.map_err(|_| "tree_edit_failed")?;
editor.write(write_tree).map_err(|_| "tree_write_failed")
}
fn lookup_verified_tree_entry(
repository: &gix::Repository,
root_tree_oid: gix::hash::ObjectId,
path: &str,
) -> Result<Option<(gix::objs::tree::EntryKind, gix::hash::ObjectId)>, &'static str> {
let components = path.split('/').collect::<Vec<_>>();
let mut tree_oid = root_tree_oid;
for (index, component) in components.iter().enumerate() {
let tree = load_verified_object(
repository,
tree_oid,
gix::objs::Kind::Tree,
MANAGED_TREE_POLICY_V3.max_single_tree_object_bytes,
"base_tree_unavailable",
"base_tree_unavailable",
"source_tree_object_limit_exceeded",
"base_tree_identity_mismatch",
)?
.try_into_tree()
.map_err(|_| "base_tree_unavailable")?;
let entry = tree
.iter()
.find_map(|entry| match entry {
Ok(entry) if entry.filename() == component.as_bytes() => Some(Ok(entry)),
Ok(_) => None,
Err(_) => Some(Err("base_tree_unavailable")),
})
.transpose()?;
let Some(entry) = entry else {
return Ok(None);
};
if index + 1 == components.len() {
return Ok(Some((entry.mode().kind(), entry.object_id())));
}
if entry.mode().kind() != gix::objs::tree::EntryKind::Tree {
return Err("base_path_lookup_failed");
}
tree_oid = entry.object_id();
}
Err("base_path_lookup_failed")
}
fn verify_candidate_commit_and_result(
repository: &gix::Repository,
candidate_commit_oid: gix::hash::ObjectId,
expected_base_commit_oid: gix::hash::ObjectId,
expected_candidate_tree_oid: gix::hash::ObjectId,
path: &str,
expected_result_blob_oid: gix::hash::ObjectId,
request_digest_sha256: &str,
) -> Result<(), &'static str> {
let commit = load_verified_object(
repository,
candidate_commit_oid,
gix::objs::Kind::Commit,
MANAGED_TREE_POLICY_V3.max_commit_object_bytes,
"commit_write_failed",
"commit_write_failed",
"commit_object_limit_exceeded",
"successor_commit_identity_mismatch",
)?
.try_into_commit()
.map_err(|_| "commit_write_failed")?
.decode()
.map_err(|_| "commit_write_failed")?
.into_owned()
.map_err(|_| "commit_write_failed")?;
let expected_message =
format!("maka managed workspace candidate v3\nrequest-sha256 {request_digest_sha256}");
if commit.tree != expected_candidate_tree_oid
|| commit.parents.as_slice() != [expected_base_commit_oid]
|| commit.message.as_slice() != expected_message.as_bytes()
|| !has_managed_candidate_signature(&commit)
{
return Err("successor_commit_identity_mismatch");
}
let result_entry = lookup_verified_tree_entry(repository, expected_candidate_tree_oid, path)?
.ok_or("tree_write_failed")?;
if result_entry.1 != expected_result_blob_oid {
return Err("tree_write_failed");
}
load_verified_object(
repository,
expected_result_blob_oid,
gix::objs::Kind::Blob,
MAX_IMPORT_FILE_BYTES,
"blob_write_failed",
"blob_write_failed",
"successor_content_limit_exceeded",
"blob_write_failed",
)?;
Ok(())
}
fn candidate_request_digest_sha256(
accepted_ref: &str,
expected_base_commit_oid: &str,
expected_base_tree_oid: &str,
candidate_ref: &str,
path: &str,
content: &[u8],
) -> String {
let mut hasher = Sha256::new();
hasher.update(b"maka.gitoxide.candidate-request.v1\0");
for field in [
accepted_ref.as_bytes(),
expected_base_commit_oid.as_bytes(),
expected_base_tree_oid.as_bytes(),
candidate_ref.as_bytes(),
path.as_bytes(),
content,
] {
hasher.update((field.len() as u64).to_be_bytes());
hasher.update(field);
}
format!("{:x}", hasher.finalize())
}
fn is_canonical_successor_path_v3(path: &str) -> bool {
let policy = MANAGED_TREE_POLICY_V3;
!path.is_empty()
&& !path.starts_with('/')
&& !path.contains('\0')
&& path.len() as u64 <= policy.max_relative_path_bytes
&& fold_managed_path_v3(path).len() as u64 <= policy.max_folded_relative_path_bytes
&& path.split('/').all(|component| {
is_supported_source_component(component)
&& component.len() as u64 <= policy.max_component_bytes
})
}
fn read_tree_file(
repository_path: PathBuf,
accepted_commit_oid: String,
path: String,
managed_tree_policy_version: u8,
) -> Result<ExitCode, &'static str> {
if managed_tree_policy_version != MANAGED_TREE_POLICY_VERSION {
return Err("unsupported_managed_tree_policy");
}
if !is_canonical_successor_path_v3(&path) {
return Err("invalid_tree_file_path");
}
let repository = open_repository(repository_path)?;
let (accepted_commit, accepted_tree) =
accepted_commit_identity(&repository, &accepted_commit_oid)?;
let components = path.split('/').collect::<Vec<_>>();
let mut tree_oid = accepted_tree;
let mut final_entry = None;
for (index, component) in components.iter().enumerate() {
let tree = load_verified_object(
&repository,
tree_oid,
gix::objs::Kind::Tree,
MANAGED_TREE_POLICY_V3.max_single_tree_object_bytes,
"tree_file_unavailable",
"tree_file_invalid",
"source_tree_object_limit_exceeded",
"source_tree_identity_mismatch",
)?
.try_into_tree()
.map_err(|_| "tree_file_invalid")?;
let entry = tree
.iter()
.find_map(|entry| match entry {
Ok(entry) if entry.filename() == component.as_bytes() => Some(Ok(entry)),
Ok(_) => None,
Err(_) => Some(Err("tree_file_lookup_failed")),
})
.ok_or("tree_file_unavailable")??;
if index + 1 == components.len() {
final_entry = Some((entry.mode().kind(), entry.object_id()));
} else if entry.mode().kind() == gix::objs::tree::EntryKind::Tree {
tree_oid = entry.object_id();
} else {
return Err("tree_file_invalid");
}
}
let (kind, blob_oid) = final_entry.ok_or("tree_file_unavailable")?;
if !matches!(
kind,
gix::objs::tree::EntryKind::Blob | gix::objs::tree::EntryKind::BlobExecutable
) {
return Err("tree_file_invalid");
}
let blob = load_verified_object(
&repository,
blob_oid,
gix::objs::Kind::Blob,
MAX_TREE_FILE_BYTES,
"tree_file_unavailable",
"tree_file_invalid",
"tree_file_size_limit_exceeded",
"tree_file_identity_mismatch",
)?
.try_into_blob()
.map_err(|_| "tree_file_invalid")?;
let content = std::str::from_utf8(&blob.data)
.map_err(|_| "tree_file_not_utf8")?
.to_owned();
write_response(&Response::TreeFileRead {
protocol_version: PROTOCOL_VERSION,
object_format: "sha1",
accepted_commit_oid: accepted_commit.to_string(),
accepted_tree_oid: accepted_tree.to_string(),
blob_oid: blob_oid.to_string(),
path,
content,
bytes_read: blob.data.len() as u64,
managed_tree_policy_version: MANAGED_TREE_POLICY_VERSION,
});
Ok(ExitCode::SUCCESS)
}
fn accepted_commit_identity(
repository: &gix::Repository,
accepted_commit_oid: &str,
) -> Result<(gix::hash::ObjectId, gix::hash::ObjectId), &'static str> {
if repository.object_hash() != gix::hash::Kind::Sha1 {
return Err("unsupported_object_format");
}
let accepted_commit = gix::hash::ObjectId::from_hex(accepted_commit_oid.as_bytes())
.map_err(|_| "invalid_accepted_commit_oid")?;
let commit = load_verified_object(
repository,
accepted_commit,
gix::objs::Kind::Commit,
MANAGED_TREE_POLICY_V3.max_commit_object_bytes,
"accepted_commit_unavailable",
"accepted_commit_unavailable",
"commit_object_limit_exceeded",
"head_commit_identity_mismatch",
)?
.try_into_commit()
.map_err(|_| "accepted_commit_unavailable")?;
let accepted_tree = commit
.tree_id()
.map_err(|_| "accepted_tree_unavailable")?
.detach();
Ok((accepted_commit, accepted_tree))
}
fn claim_fresh_import_destination(path: &Path) -> Result<gix::Repository, &'static str> {
match fs::create_dir(path) {
Ok(()) => {}
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
return Err("import_destination_not_fresh");
}
Err(_) => return Err("import_destination_create_failed"),
}
gix::ThreadSafeRepository::init_opts(
path,
gix::create::Kind::Bare,
gix::create::Options {
destination_must_be_empty: Some(true),
object_hash: Some(gix::hash::Kind::Sha1),
..Default::default()
},
managed_open_options(),
)
.map_err(|_| "import_destination_create_failed")
.map(|repository| repository.to_thread_local())
}
fn load_verified_object<'repo>(
repository: &'repo gix::Repository,
object_id: gix::hash::ObjectId,
expected_kind: gix::objs::Kind,
max_bytes: u64,
unavailable_reason: &'static str,
invalid_reason: &'static str,
limit_reason: &'static str,
identity_reason: &'static str,
) -> Result<gix::Object<'repo>, &'static str> {
let header = repository
.find_header(object_id)
.map_err(|_| unavailable_reason)?;
if header.kind() != expected_kind {
return Err(invalid_reason);
}
if header.size() > max_bytes {
return Err(limit_reason);
}
let object = repository
.find_object(object_id)
.map_err(|_| unavailable_reason)?;
if object.kind != expected_kind {
return Err(invalid_reason);
}
if object.data.len() as u64 > max_bytes {
return Err(limit_reason);
}
gix::objs::Data::new(&object.data, object.kind, object.id.kind())
.verify_checksum(object_id.as_ref())
.map_err(|_| identity_reason)?;
Ok(object)
}
fn assert_import_destination_parent(destination: &Path) -> Result<(), &'static str> {
if !destination.is_absolute() {
return Err("import_destination_parent_untrusted");
}
let parent = destination
.parent()
.ok_or("import_destination_parent_untrusted")?;
let mut ancestors = parent.ancestors().collect::<Vec<_>>();
ancestors.reverse();
for ancestor in ancestors {
if ancestor.as_os_str().is_empty() {
continue;
}
let metadata =
fs::symlink_metadata(ancestor).map_err(|_| "import_destination_parent_untrusted")?;
if metadata.file_type().is_symlink() || is_windows_reparse_point(&metadata) {
return Err("import_destination_parent_untrusted");
}
}
Ok(())
}
#[cfg(windows)]
fn is_windows_reparse_point(metadata: &fs::Metadata) -> bool {
use std::os::windows::fs::MetadataExt;
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0
}
#[cfg(not(windows))]
fn is_windows_reparse_point(_metadata: &fs::Metadata) -> bool {
false
}
fn walk_verified_source_tree(
source: &gix::Repository,
destination: Option<&gix::Repository>,
tree_oid: gix::hash::ObjectId,
prefix: &str,
depth: u64,
policy: ManagedTreePolicy,
stats: &mut ManagedTreeStats,
) -> Result<(), &'static str> {
let tree = load_verified_object(
source,
tree_oid,
gix::objs::Kind::Tree,
policy.max_single_tree_object_bytes,
"source_tree_unavailable",
"source_tree_invalid",
"source_tree_object_limit_exceeded",
"source_tree_identity_mismatch",
)?
.try_into_tree()
.map_err(|_| "source_tree_invalid")?;
stats.enter_tree(depth, tree.data.len() as u64, policy)?;
assert_canonical_tree_modes(&tree.data)?;
let mut previous_entry: Option<(Vec<u8>, bool)> = None;
for entry in tree.iter() {
let entry = entry.map_err(|_| "source_tree_invalid")?;
let component =
std::str::from_utf8(entry.filename()).map_err(|_| "unsupported_source_path")?;
if !is_supported_source_component(component)
|| component.len() as u64 > policy.max_component_bytes
{
return Err("unsupported_source_path");
}
let is_tree = matches!(entry.mode().kind(), gix::objs::tree::EntryKind::Tree);
if previous_entry.as_ref().is_some_and(|(name, tree)| {
compare_git_tree_entry_names(name, *tree, entry.filename(), is_tree)
!= std::cmp::Ordering::Less
}) {
return Err("source_tree_not_sorted");
}
previous_entry = Some((entry.filename().to_vec(), is_tree));
let relative_path = if prefix.is_empty() {
component.to_owned()
} else {
format!("{prefix}/{component}")
};
stats.observe_entry(&relative_path, policy)?;
match entry.mode().kind() {
gix::objs::tree::EntryKind::Tree => {
walk_verified_source_tree(
source,
destination,
entry.object_id(),
&relative_path,
depth.checked_add(1).ok_or("source_tree_depth_exceeded")?,
policy,
stats,
)?;
}
gix::objs::tree::EntryKind::Blob | gix::objs::tree::EntryKind::BlobExecutable => {
let blob_oid = entry.object_id();
let is_attributes = component == ".gitattributes";
let blob = load_verified_object(
source,
blob_oid,
gix::objs::Kind::Blob,
if is_attributes {
MAX_ATTRIBUTES_FILE_BYTES
} else {
policy.max_file_bytes
},
"source_blob_unavailable",
"source_blob_invalid",
if is_attributes {
"source_attributes_limit_exceeded"
} else {
"source_file_limit_exceeded"
},
"source_blob_identity_mismatch",
)?
.try_into_blob()
.map_err(|_| "source_blob_invalid")?;
if is_attributes {
validate_managed_attributes_v2(&blob.data)?;
}
stats.observe_blob(blob.data.len() as u64, policy)?;
if let Some(destination) = destination {
let copied_blob = destination
.write_blob(&blob.data)
.map_err(|_| "source_blob_copy_failed")?
.detach();
if copied_blob != blob_oid {
return Err("source_blob_identity_mismatch");
}
}
}
_ => return Err("unsupported_source_entry_kind"),
}
}
if let Some(destination) = destination {
let copied_tree = destination
.write_object(tree.decode().map_err(|_| "source_tree_invalid")?)
.map_err(|_| "source_tree_copy_failed")?
.detach();
if copied_tree != tree_oid {
return Err("source_tree_identity_mismatch");
}
}
Ok(())
}
fn compare_git_tree_entry_names(
left_name: &[u8],
left_is_tree: bool,
right_name: &[u8],
right_is_tree: bool,
) -> std::cmp::Ordering {
let common = left_name.len().min(right_name.len());
match left_name[..common].cmp(&right_name[..common]) {
std::cmp::Ordering::Equal => {
let left_suffix =
left_name
.get(common)
.copied()
.unwrap_or(if left_is_tree { b'/' } else { 0 });
let right_suffix =
right_name
.get(common)
.copied()
.unwrap_or(if right_is_tree { b'/' } else { 0 });
left_suffix.cmp(&right_suffix)
}
ordering => ordering,
}
}
fn assert_canonical_tree_modes(mut data: &[u8]) -> Result<(), &'static str> {
const SHA1_OID_BYTES: usize = 20;
while !data.is_empty() {
let mode_end = data
.iter()
.position(|byte| *byte == b' ')
.ok_or("source_tree_invalid")?;
let mode = &data[..mode_end];
if mode != b"40000" && mode != b"100644" && mode != b"100755" {
return Err("source_tree_noncanonical_mode");
}
data = &data[mode_end + 1..];
let filename_end = data
.iter()
.position(|byte| *byte == 0)
.ok_or("source_tree_invalid")?;
data = &data[filename_end + 1..];
if data.len() < SHA1_OID_BYTES {
return Err("source_tree_invalid");
}
data = &data[SHA1_OID_BYTES..];
}
Ok(())
}
fn is_supported_source_component(component: &str) -> bool {
let folded_component = fold_managed_path_v3(component);
!component.is_empty()
&& component != "."
&& component != ".."
&& !component.contains('/')
&& !component.contains('\\')
&& !component.contains('\0')
&& !component
.chars()
.any(|character| character <= '\u{001f}' || "<>:\"|?*".contains(character))
&& !component.ends_with('.')
&& !component.ends_with(' ')
&& !is_windows_reserved_device_name(component)
&& folded_component != ".git"
&& (component == ".gitattributes" || folded_component != ".gitattributes")
}
fn fold_managed_path_v3(value: &str) -> String {
value
.chars()
.filter(|character| !is_git_hfs_ignorable(*character))
.nfc()
.default_case_fold()
.nfc()
.collect()
}
fn is_git_hfs_ignorable(character: char) -> bool {
matches!(
character,
'\u{200c}'..='\u{200f}' | '\u{202a}'..='\u{202e}' | '\u{206a}'..='\u{206f}' | '\u{feff}'
)
}
fn validate_managed_attributes_v2(data: &[u8]) -> Result<(), &'static str> {
let content = std::str::from_utf8(data).map_err(|_| "unsupported_source_attributes")?;
for raw_line in content.split('\n') {
if raw_line.len() >= MAX_GIT_ATTRIBUTES_LINE_BYTES_V2 {
return Err("unsupported_source_attributes");
}
if raw_line.is_empty() {
continue;
}
if let Some(comment) = raw_line.strip_prefix('#') {
if comment.chars().any(char::is_control) {
return Err("unsupported_source_attributes");
}
continue;
}
if raw_line == "* text=auto eol=lf" {
continue;
}
if raw_line
.strip_suffix(" export-ignore")
.is_some_and(is_portable_export_ignore_pattern_v2)
{
continue;
}
return Err("unsupported_source_attributes");
}
Ok(())
}
fn is_portable_export_ignore_pattern_v2(pattern: &str) -> bool {
if !pattern.starts_with('/') || pattern.len() <= 1 || pattern.len() > 4096 {
return false;
}
pattern[1..]
.split('/')
.all(is_literal_attributes_component_v2)
}
fn is_literal_attributes_component_v2(component: &str) -> bool {
!component.is_empty()
&& is_supported_source_component(component)
&& !component.contains(['[', ']'])
&& !component
.chars()
.any(|character| character.is_whitespace() || character.is_control())
}
fn is_windows_reserved_device_name(component: &str) -> bool {
let stem = component.split('.').next().unwrap_or_default();
let folded = stem.to_ascii_uppercase();
matches!(
folded.as_str(),
"CON" | "PRN" | "AUX" | "NUL" | "CONIN$" | "CONOUT$"
) || folded
.strip_prefix("COM")
.is_some_and(|suffix| matches!(suffix, "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"))
|| folded.strip_prefix("LPT").is_some_and(|suffix| {
matches!(suffix, "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9")
})
|| matches!(
folded.as_str(),
"COM¹" | "COM²" | "COM³" | "LPT¹" | "LPT²" | "LPT³"
)
}
#[derive(Clone, Copy)]
struct ManagedTreePolicy {
max_depth: u64,
max_tree_visits: u64,
max_entries: u64,
max_total_path_bytes: u64,
max_total_folded_path_bytes: u64,
max_component_bytes: u64,
max_relative_path_bytes: u64,
max_folded_relative_path_bytes: u64,
max_files: u64,
max_file_bytes: u64,
max_bytes: u64,
max_commit_object_bytes: u64,
max_single_tree_object_bytes: u64,
max_total_tree_object_bytes: u64,
}
#[derive(Default)]
struct ManagedTreeStats {
tree_visits: u64,
entries: u64,
total_path_bytes: u64,
total_folded_path_bytes: u64,
files: u64,
bytes: u64,
tree_object_bytes: u64,
folded_paths: HashSet<String>,
}
impl ManagedTreeStats {
fn enter_tree(
&mut self,
depth: u64,
object_bytes: u64,
policy: ManagedTreePolicy,
) -> Result<(), &'static str> {
if depth > policy.max_depth {
return Err("source_tree_depth_exceeded");
}
self.tree_visits = self
.tree_visits
.checked_add(1)
.filter(|visits| *visits <= policy.max_tree_visits)
.ok_or("source_tree_visit_limit_exceeded")?;
self.tree_object_bytes = self
.tree_object_bytes
.checked_add(object_bytes)
.filter(|bytes| *bytes <= policy.max_total_tree_object_bytes)
.ok_or("source_tree_object_byte_limit_exceeded")?;
Ok(())
}
fn observe_entry(
&mut self,
relative_path: &str,
policy: ManagedTreePolicy,
) -> Result<(), &'static str> {
let path_bytes = relative_path.len() as u64;
if path_bytes > policy.max_relative_path_bytes {
return Err("source_path_length_exceeded");
}
self.entries = self
.entries
.checked_add(1)
.filter(|entries| *entries <= policy.max_entries)
.ok_or("source_tree_entry_limit_exceeded")?;
self.total_path_bytes = self
.total_path_bytes
.checked_add(path_bytes)
.filter(|bytes| *bytes <= policy.max_total_path_bytes)
.ok_or("source_path_byte_limit_exceeded")?;
let folded_path = fold_managed_path_v3(relative_path);
let folded_path_bytes = folded_path.len() as u64;
if folded_path_bytes > policy.max_folded_relative_path_bytes {
return Err("source_folded_path_length_exceeded");
}
self.total_folded_path_bytes = self
.total_folded_path_bytes
.checked_add(folded_path_bytes)
.filter(|bytes| *bytes <= policy.max_total_folded_path_bytes)
.ok_or("source_folded_path_byte_limit_exceeded")?;
if !self.folded_paths.insert(folded_path) {
return Err("source_path_collision");
}
Ok(())
}
fn observe_blob(&mut self, size: u64, policy: ManagedTreePolicy) -> Result<(), &'static str> {
if size > policy.max_file_bytes {
return Err("source_file_limit_exceeded");
}
self.files = self
.files
.checked_add(1)
.filter(|files| *files <= policy.max_files)
.ok_or("source_file_limit_exceeded")?;
self.bytes = self
.bytes
.checked_add(size)
.filter(|bytes| *bytes <= policy.max_bytes)
.ok_or("source_byte_limit_exceeded")?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
fn tiny_policy() -> ManagedTreePolicy {
ManagedTreePolicy {
max_depth: 1,
max_tree_visits: 2,
max_entries: 2,
max_total_path_bytes: 5,
max_total_folded_path_bytes: 5,
max_component_bytes: 3,
max_relative_path_bytes: 4,
max_folded_relative_path_bytes: 4,
max_files: 1,
max_file_bytes: 3,
max_bytes: 3,
max_commit_object_bytes: 3,
max_single_tree_object_bytes: 3,
max_total_tree_object_bytes: 5,
}
}
#[test]
fn candidate_editor_rejects_a_tree_replaced_after_the_verified_walk() {
let root = tempfile::tempdir().unwrap();
run_git(root.path(), ["init", "--quiet"]);
fs::create_dir(root.path().join("dir")).unwrap();
fs::write(root.path().join("dir/file.txt"), b"base\n").unwrap();
run_git(root.path(), ["add", "dir/file.txt"]);
let base_tree = run_git_output(root.path(), ["write-tree"]);
let nested_tree = run_git_output(root.path(), ["rev-parse", &format!("{base_tree}:dir")]);
fs::write(root.path().join("dir/file.txt"), b"replacement\n").unwrap();
run_git(root.path(), ["add", "dir/file.txt"]);
let replacement_root = run_git_output(root.path(), ["write-tree"]);
let replacement_tree = run_git_output(
root.path(),
["rev-parse", &format!("{replacement_root}:dir")],
);
let repository = managed_open_options()
.open(root.path())
.unwrap()
.to_thread_local();
let base_tree_oid = gix::hash::ObjectId::from_hex(base_tree.as_bytes()).unwrap();
let mut stats = ManagedTreeStats::default();
walk_verified_source_tree(
&repository,
None,
base_tree_oid,
"",
0,
MANAGED_TREE_POLICY_V3,
&mut stats,
)
.unwrap();
fs::remove_file(loose_object_path(root.path(), &nested_tree)).unwrap();
fs::copy(
loose_object_path(root.path(), &replacement_tree),
loose_object_path(root.path(), &nested_tree),
)
.unwrap();
let result_blob = repository.write_blob(b"result\n").unwrap().detach();
assert_eq!(
write_candidate_tree_from_verified_base(
&repository,
base_tree_oid,
"dir/file.txt",
gix::objs::tree::EntryKind::Blob,
result_blob,
),
Err("tree_edit_failed")
);
}
#[test]
fn candidate_self_check_rejects_a_corrupt_result_blob() {
use gix::bstr::ByteSlice;
let root = tempfile::tempdir().unwrap();
run_git(root.path(), ["init", "--quiet"]);
fs::write(root.path().join("base.txt"), b"base\n").unwrap();
run_git(root.path(), ["add", "base.txt"]);
run_git(
root.path(),
[
"-c",
"user.name=Maka Test",
"-c",
"user.email=maka@example.invalid",
"commit",
"--quiet",
"-m",
"base",
],
);
let base_commit = run_git_output(root.path(), ["rev-parse", "HEAD"]);
let base_tree = run_git_output(root.path(), ["rev-parse", "HEAD^{tree}"]);
let repository = managed_open_options()
.open(root.path())
.unwrap()
.to_thread_local();
let base_commit_oid = gix::hash::ObjectId::from_hex(base_commit.as_bytes()).unwrap();
let base_tree_oid = gix::hash::ObjectId::from_hex(base_tree.as_bytes()).unwrap();
let result_blob_oid = repository.write_blob(b"result\n").unwrap().detach();
let candidate_tree_oid = write_candidate_tree_from_verified_base(
&repository,
base_tree_oid,
"result.txt",
gix::objs::tree::EntryKind::Blob,
result_blob_oid,
)
.unwrap();
let request_digest = "0".repeat(64);
let signature = gix::actor::SignatureRef {
name: b"Maka Workspace Service".as_bstr(),
email: b"workspace@maka.invalid".as_bstr(),
time: "946684800 +0000",
};
let candidate_commit_oid = repository
.new_commit_as(
signature,
signature,
format!("maka managed workspace candidate v3\nrequest-sha256 {request_digest}"),
candidate_tree_oid,
[base_commit_oid],
)
.unwrap()
.id()
.detach();
let replacement_blob_oid = repository.write_blob(b"replacement\n").unwrap().detach();
drop(repository);
let result_object_path = loose_object_path(root.path(), &result_blob_oid.to_string());
fs::rename(
&result_object_path,
root.path().join("original-result-object"),
)
.unwrap();
fs::copy(
loose_object_path(root.path(), &replacement_blob_oid.to_string()),
result_object_path,
)
.unwrap();
let repository = managed_open_options()
.open(root.path())
.unwrap()
.to_thread_local();
assert_eq!(
verify_candidate_commit_and_result(
&repository,
candidate_commit_oid,
base_commit_oid,
candidate_tree_oid,
"result.txt",
result_blob_oid,
&request_digest,
),
Err("blob_write_failed")
);
}
fn run_git<const N: usize>(root: &Path, args: [&str; N]) {
assert!(
Command::new("git")
.args(args)
.current_dir(root)
.status()
.unwrap()
.success()
);
}
fn run_git_output<const N: usize>(root: &Path, args: [&str; N]) -> String {
let output = Command::new("git")
.args(args)
.current_dir(root)
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout).unwrap().trim().to_owned()
}
fn loose_object_path(root: &Path, oid: &str) -> PathBuf {
root.join(".git")
.join("objects")
.join(&oid[..2])
.join(&oid[2..])
}
#[test]
fn managed_tree_budget_bounds_depth_visits_and_entries() {
let policy = tiny_policy();
let mut stats = ManagedTreeStats::default();
assert_eq!(stats.enter_tree(0, 2, policy), Ok(()));
assert_eq!(stats.enter_tree(1, 3, policy), Ok(()));
assert_eq!(
stats.enter_tree(1, 0, policy),
Err("source_tree_visit_limit_exceeded")
);
let mut stats = ManagedTreeStats::default();
assert_eq!(
stats.enter_tree(2, 0, policy),
Err("source_tree_depth_exceeded")
);
assert_eq!(stats.observe_entry("a", policy), Ok(()));
assert_eq!(stats.observe_entry("bb", policy), Ok(()));
assert_eq!(
stats.observe_entry("c", policy),
Err("source_tree_entry_limit_exceeded")
);
}
#[test]
fn managed_tree_budget_bounds_single_and_total_tree_object_bytes() {
let policy = tiny_policy();
let mut stats = ManagedTreeStats::default();
assert_eq!(stats.enter_tree(0, 3, policy), Ok(()));
assert_eq!(
stats.enter_tree(1, 3, policy),
Err("source_tree_object_byte_limit_exceeded")
);
}
#[test]
fn managed_tree_budget_bounds_paths_and_blob_bytes() {
let policy = tiny_policy();
let mut stats = ManagedTreeStats::default();
assert_eq!(
stats.observe_entry("abcde", policy),
Err("source_path_length_exceeded")
);
assert_eq!(stats.observe_entry("abc", policy), Ok(()));
assert_eq!(
stats.observe_entry("def", policy),
Err("source_path_byte_limit_exceeded")
);
let mut stats = ManagedTreeStats::default();
assert_eq!(
stats.observe_blob(4, policy),
Err("source_file_limit_exceeded")
);
assert_eq!(stats.observe_blob(3, policy), Ok(()));
assert_eq!(
stats.observe_blob(1, policy),
Err("source_file_limit_exceeded")
);
}
#[test]
fn managed_tree_policy_rejects_non_portable_components() {
for component in [
"CON",
"NUL.txt",
"COM1.log",
"LPT9",
"a:b",
"control\u{001f}",
"trailing.",
"trailing ",
".git.",
".git ",
"com¹",
"lpt².txt",
] {
assert!(
!is_supported_source_component(component),
"component must be rejected by portable policy: {component:?}"
);
}
}
#[test]
fn managed_tree_policy_uses_full_unicode_casefold_after_nfc() {
let policy = MANAGED_TREE_POLICY_V3;
for (first, second) in [
("Σ.txt", "ς.txt"),
("STRASSE.txt", "Straße.txt"),
("é.txt", "e\u{301}.txt"),
] {
let mut stats = ManagedTreeStats::default();
assert_eq!(stats.observe_entry(first, policy), Ok(()));
assert_eq!(
stats.observe_entry(second, policy),
Err("source_path_collision"),
"paths must collide under the versioned fold key: {first:?}, {second:?}"
);
}
}
#[test]
fn managed_tree_policy_bounds_folded_keys_independently() {
let policy = ManagedTreePolicy {
max_relative_path_bytes: 2,
max_folded_relative_path_bytes: 2,
..MANAGED_TREE_POLICY_V3
};
let mut stats = ManagedTreeStats::default();
assert_eq!(
stats.observe_entry("İ", policy),
Err("source_folded_path_length_exceeded")
);
let policy = ManagedTreePolicy {
max_total_path_bytes: 3,
max_total_folded_path_bytes: 3,
..MANAGED_TREE_POLICY_V3
};
let mut stats = ManagedTreeStats::default();
assert_eq!(stats.observe_entry("İ", policy), Ok(()));
assert_eq!(
stats.observe_entry("A", policy),
Err("source_folded_path_byte_limit_exceeded")
);
}
#[test]
fn managed_attributes_v2_accepts_only_the_deterministic_subset() {
assert_eq!(
validate_managed_attributes_v2(
b"# portable Maka attributes\n\n* text=auto eol=lf\n/docs export-ignore\n"
),
Ok(())
);
for unsupported in [
b"*.bin filter=lfs\n".as_slice(),
b"* working-tree-encoding=UTF-16\n".as_slice(),
b"* ident\n".as_slice(),
b"* text=auto eol=crlf\n".as_slice(),
b"/*.log export-ignore\n".as_slice(),
b"* text=auto eol=lf unknown\n".as_slice(),
&[0xff, b'\n'],
] {
assert_eq!(
validate_managed_attributes_v2(unsupported),
Err("unsupported_source_attributes")
);
}
}
#[test]
fn managed_attributes_v2_rejects_bracket_globs() {
for unsupported in [
b"/[ab].txt export-ignore\n".as_slice(),
b"/file[0-9].txt export-ignore\n".as_slice(),
] {
assert_eq!(
validate_managed_attributes_v2(unsupported),
Err("unsupported_source_attributes")
);
}
}
#[test]
fn managed_attributes_v2_rejects_non_git_blank_characters() {
for unsupported in [
"\u{00a0}* text=auto eol=lf\n".as_bytes(),
b"\x0b* text=auto eol=lf\n".as_slice(),
b"\x0c* text=auto eol=lf\n".as_slice(),
] {
assert_eq!(
validate_managed_attributes_v2(unsupported),
Err("unsupported_source_attributes")
);
}
}
#[test]
fn managed_attributes_v2_matches_gits_line_byte_limit() {
let accepted = format!("/{} export-ignore", "a".repeat(2032));
assert_eq!(accepted.len(), 2047);
assert_eq!(validate_managed_attributes_v2(accepted.as_bytes()), Ok(()));
let rejected = format!("/{} export-ignore", "a".repeat(2033));
assert_eq!(rejected.len(), 2048);
assert_eq!(
validate_managed_attributes_v2(rejected.as_bytes()),
Err("unsupported_source_attributes")
);
}
#[test]
fn managed_attributes_v2_rejects_noncanonical_serializations() {
for unsupported in [
b"* eol=lf text=auto\n".as_slice(),
b" * text=auto eol=lf\n".as_slice(),
b"* text=auto eol=lf\n".as_slice(),
b"* text=auto eol=lf \n".as_slice(),
b"* text=auto eol=lf\r\n".as_slice(),
b"/docs export-ignore\n".as_slice(),
b" /docs export-ignore\n".as_slice(),
] {
assert_eq!(
validate_managed_attributes_v2(unsupported),
Err("unsupported_source_attributes")
);
}
}
#[test]
fn canonical_tree_order_uses_gits_directory_suffix_rule() {
assert_eq!(
compare_git_tree_entry_names(b"foo", false, b"foo.bar", false),
std::cmp::Ordering::Less
);
assert_eq!(
compare_git_tree_entry_names(b"foo.bar", false, b"foo", true),
std::cmp::Ordering::Less
);
assert_eq!(
compare_git_tree_entry_names(b"foo", true, b"foo0", false),
std::cmp::Ordering::Less
);
assert_eq!(
compare_git_tree_entry_names(b"same", false, b"same", false),
std::cmp::Ordering::Equal
);
}
}
fn reject_unsupported_object_format(object_format: String) -> ExitCode {
write_response(&Response::RepositoryRejected {
protocol_version: PROTOCOL_VERSION,
reason: "unsupported_object_format",
object_format,
supported_object_formats: ["sha1"],
});
ExitCode::from(2)
}
fn read_request() -> Result<Request, &'static str> {
let mut bytes = Vec::new();
io::stdin()
.take(MAX_REQUEST_BYTES + 1)
.read_to_end(&mut bytes)
.map_err(|_| "request_read_failed")?;
if bytes.len() as u64 > MAX_REQUEST_BYTES {
return Err("request_too_large");
}
serde_json::from_slice(&bytes).map_err(|_| "invalid_request")
}
fn write_response(response: &Response<'_>) {
let encoded = serde_json::to_string(response).expect("closed response shape must serialize");
println!("{encoded}");
}