use base64::Engine;
use chrono::{DateTime, Utc};
use http::StatusCode;
use serde_json::{json, Map, Value};
use fakecloud_core::service::{AwsRequest, AwsResponse, AwsServiceError};
use crate::state::*;
use crate::validate;
pub(crate) fn ok(v: Value) -> Result<AwsResponse, AwsServiceError> {
Ok(AwsResponse::json_value(StatusCode::OK, v))
}
pub(crate) fn ts(dt: DateTime<Utc>) -> Value {
json!(dt.timestamp_millis() as f64 / 1000.0)
}
pub(crate) fn err(code: &str, msg: impl Into<String>) -> AwsServiceError {
AwsServiceError::aws_error(StatusCode::BAD_REQUEST, code, msg)
}
pub(crate) fn body(req: &AwsRequest) -> Value {
serde_json::from_slice(&req.body).unwrap_or(Value::Null)
}
pub(crate) fn str_field(b: &Value, key: &str) -> Option<String> {
b.get(key).and_then(|v| v.as_str()).map(str::to_string)
}
pub(crate) fn caller_arn(req: &AwsRequest) -> String {
req.principal
.as_ref()
.map(|p| p.arn.clone())
.unwrap_or_else(|| format!("arn:aws:iam::{}:root", req.account_id))
}
pub(crate) fn sha1_hex(data: &[u8]) -> String {
use sha1::{Digest, Sha1};
let mut h = Sha1::new();
h.update(data);
format!("{:x}", h.finalize())
}
pub(crate) fn blob_id_for(content: &[u8]) -> String {
let header = format!("blob {}\0", content.len());
let mut buf = header.into_bytes();
buf.extend_from_slice(content);
sha1_hex(&buf)
}
pub(crate) fn tree_id_for(entries: &std::collections::BTreeMap<String, FileEntry>) -> String {
let mut buf = String::new();
for (path, e) in entries {
buf.push_str(&format!("{}\t{}\t{}\n", e.mode, path, e.blob_id));
}
sha1_hex(buf.as_bytes())
}
pub(crate) fn new_uuid() -> String {
uuid::Uuid::new_v4().to_string()
}
pub(crate) fn fresh_object_id() -> String {
sha1_hex(uuid::Uuid::new_v4().as_bytes())
}
pub(crate) fn is_object_id(s: &str) -> bool {
s.len() == 40 && s.chars().all(|c| c.is_ascii_hexdigit())
}
pub(crate) fn file_mode_to_git(mode: &str) -> &'static str {
match mode {
"EXECUTABLE" => "100755",
"SYMLINK" => "120000",
_ => "100644",
}
}
pub(crate) fn git_mode_to_file(mode: &str) -> &'static str {
match mode {
"100755" => "EXECUTABLE",
"120000" => "SYMLINK",
_ => "NORMAL",
}
}
pub(crate) fn require(b: &Value, key: &str, err_code: &str) -> Result<String, AwsServiceError> {
match str_field(b, key) {
Some(v) if !v.is_empty() => Ok(v),
_ => Err(err(err_code, format!("{key} is required."))),
}
}
pub(crate) fn require_repository_name(b: &Value) -> Result<String, AwsServiceError> {
let v = require(b, "repositoryName", "RepositoryNameRequiredException")?;
if validate::valid_repository_name(&v) {
Ok(v)
} else {
Err(err(
"InvalidRepositoryNameException",
"The repository name is not valid. Repository names must be between 1 and 100 characters and can only contain letters, numbers, periods, underscores, and dashes.",
))
}
}
pub(crate) fn check_enum(
b: &Value,
key: &str,
set: &[&str],
err_code: &str,
) -> Result<(), AwsServiceError> {
if let Some(v) = str_field(b, key) {
if !validate::is_enum(set, &v) {
return Err(err(
err_code,
format!("{v} is not a valid value for {key}."),
));
}
}
Ok(())
}
pub(crate) fn repo_arn(region: &str, account: &str, name: &str) -> String {
format!("arn:aws:codecommit:{region}:{account}:{name}")
}
pub(crate) fn is_codecommit_arn(s: &str) -> bool {
let parts: Vec<&str> = s.splitn(6, ':').collect();
parts.len() == 6 && parts[0] == "arn" && parts[2] == "codecommit"
}
pub(crate) fn repo_name_from_arn(s: &str) -> Option<String> {
let parts: Vec<&str> = s.splitn(6, ':').collect();
if parts.len() == 6 && parts[0] == "arn" && parts[2] == "codecommit" {
Some(parts[5].to_string())
} else {
None
}
}
pub(crate) fn public_comment(stored: &Value) -> Value {
let mut c = stored.clone();
if let Some(obj) = c.as_object_mut() {
obj.retain(|k, _| !k.starts_with("_ctx"));
}
c
}
pub(crate) fn repo_not_found(name: &str) -> AwsServiceError {
err(
"RepositoryDoesNotExistException",
format!("The repository {name} does not exist."),
)
}
pub(crate) fn pr_not_found() -> AwsServiceError {
err(
"PullRequestDoesNotExistException",
"The specified pull request does not exist.",
)
}
pub(crate) fn template_not_found() -> AwsServiceError {
err(
"ApprovalRuleTemplateDoesNotExistException",
"The specified approval rule template does not exist.",
)
}
pub(crate) fn normalize_rule_content(content: &str) -> String {
if serde_json::from_str::<Value>(content).is_err() {
return content.to_string();
}
let mut out = String::with_capacity(content.len());
let mut in_string = false;
let mut escaped = false;
for c in content.chars() {
if in_string {
out.push(c);
if escaped {
escaped = false;
} else if c == '\\' {
escaped = true;
} else if c == '"' {
in_string = false;
}
} else if c == '"' {
in_string = true;
out.push(c);
} else if !c.is_whitespace() {
out.push(c);
}
}
out
}
pub(crate) fn sha256_hex(data: &[u8]) -> String {
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
h.update(data);
format!("{:x}", h.finalize())
}
pub(crate) fn pool_matches(pattern: &str, arn: &str) -> bool {
if !pattern.contains('*') {
return pattern == arn;
}
let parts: Vec<&str> = pattern.split('*').collect();
let mut pos = 0usize;
for (i, part) in parts.iter().enumerate() {
if part.is_empty() {
continue;
}
if i == 0 {
if !arn[pos..].starts_with(part) {
return false;
}
pos += part.len();
} else if i == parts.len() - 1 {
if !arn[pos..].ends_with(part) {
return false;
}
} else if let Some(idx) = arn[pos..].find(part) {
pos += idx + part.len();
} else {
return false;
}
}
true
}
pub(crate) fn rule_satisfied(
content: &str,
approvers: &std::collections::BTreeSet<String>,
) -> bool {
let Ok(v) = serde_json::from_str::<Value>(content) else {
return !approvers.is_empty();
};
let Some(statements) = v.get("Statements").and_then(Value::as_array) else {
return !approvers.is_empty();
};
if statements.is_empty() {
return true;
}
for stmt in statements {
let needed = stmt
.get("NumberOfApprovalsNeeded")
.and_then(Value::as_i64)
.unwrap_or(1)
.max(0) as usize;
let pool = stmt.get("ApprovalPoolMembers").and_then(Value::as_array);
let count = match pool {
Some(pool) if !pool.is_empty() => approvers
.iter()
.filter(|a| {
pool.iter()
.filter_map(Value::as_str)
.any(|pat| pool_matches(pat, a))
})
.count(),
_ => approvers.len(),
};
if count < needed {
return false;
}
}
true
}
pub(crate) fn set_default_if_absent(repo: &mut Repo) {
let has_default = repo
.metadata
.get("defaultBranch")
.and_then(Value::as_str)
.map(|s| !s.is_empty())
.unwrap_or(false);
if !has_default {
if let Some(branch) = repo.branches.keys().next().cloned() {
if let Some(obj) = repo.metadata.as_object_mut() {
obj.insert("defaultBranch".into(), json!(branch));
}
}
}
}
pub(crate) fn build_commit(
commit_id: &str,
tree_id: &str,
parents: &[String],
b: &Value,
req: &AwsRequest,
) -> Value {
let now = Utc::now();
let date = format!("{} +0000", now.timestamp());
let name = str_field(b, "authorName")
.or_else(|| str_field(b, "name"))
.unwrap_or_else(|| caller_arn(req));
let email = str_field(b, "email").unwrap_or_default();
let user = json!({ "name": name, "email": email, "date": date });
json!({
"commitId": commit_id,
"treeId": tree_id,
"parents": parents,
"message": str_field(b, "commitMessage").unwrap_or_default(),
"author": user,
"committer": user,
"additionalData": "",
})
}
pub(crate) fn merge_parents(merge_option: &str, dest: &str, source: &str) -> Vec<String> {
if merge_option == "SQUASH_MERGE" {
vec![dest.to_string()]
} else {
vec![dest.to_string(), source.to_string()]
}
}
pub(crate) fn validate_merge_conflict_enums(b: &Value) -> Result<(), AwsServiceError> {
check_enum(
b,
"conflictDetailLevel",
validate::CONFLICT_DETAIL_LEVEL,
"InvalidConflictDetailLevelException",
)?;
check_enum(
b,
"conflictResolutionStrategy",
validate::CONFLICT_RESOLUTION_STRATEGY,
"InvalidConflictResolutionStrategyException",
)?;
Ok(())
}
pub(crate) fn refresh_source_revisions(st: &mut CodeCommitState, repo_name: &str, branch: &str) {
let now = Utc::now();
for pr in st.pull_requests.values_mut() {
if pr.get("pullRequestStatus").and_then(Value::as_str) != Some("OPEN") {
continue;
}
let is_source = pr
.get("pullRequestTargets")
.and_then(Value::as_array)
.map(|ts| {
ts.iter().any(|t| {
t.get("repositoryName").and_then(Value::as_str) == Some(repo_name)
&& t.get("sourceReference").and_then(Value::as_str) == Some(branch)
})
})
.unwrap_or(false);
if is_source {
pr["revisionId"] = json!(fresh_object_id());
pr["lastActivityDate"] = ts(now);
}
}
}
pub(crate) fn refresh_pr_targets(st: &CodeCommitState, pr: &Value) -> Value {
let mut pr = pr.clone();
if pr.get("pullRequestStatus").and_then(Value::as_str) != Some("OPEN") {
return pr;
}
if let Some(targets) = pr
.get_mut("pullRequestTargets")
.and_then(Value::as_array_mut)
{
for t in targets.iter_mut() {
let Some(repo_name) = t
.get("repositoryName")
.and_then(Value::as_str)
.map(str::to_string)
else {
continue;
};
let src_ref = t
.get("sourceReference")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
let dst_ref = t
.get("destinationReference")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
let Some(repo) = st.repositories.get(&repo_name) else {
continue;
};
let src = repo.branches.get(&src_ref).cloned();
let dst = repo.branches.get(&dst_ref).cloned();
if let Some(s) = &src {
t["sourceCommit"] = json!(s);
}
if let Some(d) = &dst {
t["destinationCommit"] = json!(d);
}
if let (Some(s), Some(d)) = (&src, &dst) {
t["mergeBase"] = match merge_base(repo, s, d) {
Some(base) => json!(base),
None => Value::Null,
};
}
}
}
pr
}
pub(crate) fn trigger_destination_ok(arn: &str, account: &str, region: &str) -> bool {
let parts: Vec<&str> = arn.splitn(6, ':').collect();
if parts.len() != 6 || parts[0] != "arn" {
return false;
}
let service = parts[2];
if service != "sns" && service != "lambda" {
return false;
}
if parts[3] != region || parts[4] != account {
return false;
}
!parts[5].is_empty()
}
pub(crate) fn resolve_commit(repo: &Repo, b: &Value, key: &str) -> Result<String, AwsServiceError> {
let spec = match str_field(b, key) {
Some(s) => s,
None => repo
.metadata
.get("defaultBranch")
.and_then(Value::as_str)
.map(str::to_string)
.ok_or_else(|| {
err(
"CommitDoesNotExistException",
"The specified commit does not exist or no default branch is set.",
)
})?,
};
if let Some(tip) = repo.branches.get(&spec) {
return Ok(tip.clone());
}
if is_object_id(&spec) && repo.commits.contains_key(&spec) {
return Ok(spec);
}
Err(err(
"CommitDoesNotExistException",
"The specified commit does not exist.",
))
}
pub(crate) fn ancestors(repo: &Repo, commit: &str) -> std::collections::BTreeSet<String> {
let mut seen = std::collections::BTreeSet::new();
let mut stack = vec![commit.to_string()];
while let Some(c) = stack.pop() {
if !seen.insert(c.clone()) {
continue;
}
if let Some(commit) = repo.commits.get(&c) {
if let Some(parents) = commit.get("parents").and_then(Value::as_array) {
for p in parents {
if let Some(pid) = p.as_str() {
stack.push(pid.to_string());
}
}
}
}
}
seen
}
pub(crate) fn is_ancestor(repo: &Repo, ancestor: &str, descendant: &str) -> bool {
ancestors(repo, descendant).contains(ancestor)
}
pub(crate) fn merge_base(repo: &Repo, a: &str, b: &str) -> Option<String> {
let depth_a = bfs_depths(repo, a);
let anc_b = ancestors(repo, b);
depth_a
.iter()
.filter(|(c, _)| anc_b.contains(*c))
.min_by(|(cx, dx), (cy, dy)| dx.cmp(dy).then_with(|| cx.cmp(cy)))
.map(|(c, _)| c.clone())
}
pub(crate) fn bfs_depths(repo: &Repo, start: &str) -> std::collections::BTreeMap<String, usize> {
let mut depths = std::collections::BTreeMap::new();
let mut frontier = vec![start.to_string()];
let mut depth = 0usize;
while !frontier.is_empty() {
let mut next = Vec::new();
for c in frontier {
if depths.contains_key(&c) {
continue;
}
depths.insert(c.clone(), depth);
if let Some(commit) = repo.commits.get(&c) {
if let Some(parents) = commit.get("parents").and_then(Value::as_array) {
for p in parents {
if let Some(pid) = p.as_str() {
if !depths.contains_key(pid) {
next.push(pid.to_string());
}
}
}
}
}
}
frontier = next;
depth += 1;
}
depths
}
pub(crate) fn entries_equal(x: Option<&FileEntry>, y: Option<&FileEntry>) -> bool {
match (x, y) {
(None, None) => true,
(Some(a), Some(b)) => a.blob_id == b.blob_id && a.mode == b.mode,
_ => false,
}
}
pub(crate) enum Resolved {
Take(Option<FileEntry>),
Conflict,
}
pub(crate) fn merge_entry(
base: Option<&FileEntry>,
source: Option<&FileEntry>,
dest: Option<&FileEntry>,
) -> Resolved {
if entries_equal(source, dest) {
Resolved::Take(source.cloned())
} else if entries_equal(source, base) {
Resolved::Take(dest.cloned())
} else if entries_equal(dest, base) {
Resolved::Take(source.cloned())
} else {
Resolved::Conflict
}
}
pub(crate) type Tree = std::collections::BTreeMap<String, FileEntry>;
pub(crate) fn merge_inputs<'a>(
repo: &'a Repo,
source: &str,
dest: &str,
empty: &'a Tree,
) -> (Option<String>, &'a Tree, &'a Tree, &'a Tree) {
let base = merge_base(repo, source, dest);
let base_tree = base
.as_ref()
.and_then(|b| repo.trees.get(b))
.unwrap_or(empty);
let src_tree = repo.trees.get(source).unwrap_or(empty);
let dst_tree = repo.trees.get(dest).unwrap_or(empty);
(base, base_tree, src_tree, dst_tree)
}
pub(crate) fn all_merge_paths(
base_tree: &std::collections::BTreeMap<String, FileEntry>,
src_tree: &std::collections::BTreeMap<String, FileEntry>,
dst_tree: &std::collections::BTreeMap<String, FileEntry>,
) -> std::collections::BTreeSet<String> {
let mut all: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
all.extend(base_tree.keys().cloned());
all.extend(src_tree.keys().cloned());
all.extend(dst_tree.keys().cloned());
all
}
pub(crate) fn conflicting_paths_in(
base_tree: &Tree,
src_tree: &Tree,
dst_tree: &Tree,
) -> Vec<String> {
let mut conflicts = Vec::new();
for path in all_merge_paths(base_tree, src_tree, dst_tree) {
if let Resolved::Conflict = merge_entry(
base_tree.get(&path),
src_tree.get(&path),
dst_tree.get(&path),
) {
conflicts.push(path);
}
}
conflicts
}
pub(crate) fn merge_trees(
repo: &Repo,
source: &str,
dest: &str,
strategy: &str,
) -> Option<std::collections::BTreeMap<String, FileEntry>> {
let empty = std::collections::BTreeMap::new();
let (_, base_tree, src_tree, dst_tree) = merge_inputs(repo, source, dest, &empty);
let mut merged: std::collections::BTreeMap<String, FileEntry> =
std::collections::BTreeMap::new();
for path in all_merge_paths(base_tree, src_tree, dst_tree) {
let s = src_tree.get(&path);
let d = dst_tree.get(&path);
let entry = match merge_entry(base_tree.get(&path), s, d) {
Resolved::Take(e) => e,
Resolved::Conflict => match strategy {
"ACCEPT_SOURCE" => s.cloned(),
"ACCEPT_DESTINATION" => d.cloned(),
_ => return None,
},
};
if let Some(e) = entry {
merged.insert(path, e);
}
}
Some(merged)
}
pub(crate) fn line_count(bytes: &[u8]) -> usize {
if bytes.is_empty() {
return 0;
}
let mut n = bytes.iter().filter(|&&b| b == b'\n').count();
if *bytes.last().unwrap() != b'\n' {
n += 1;
}
n
}
pub(crate) fn entry_bytes(repo: &Repo, entry: Option<&FileEntry>) -> Vec<u8> {
let Some(e) = entry else { return Vec::new() };
let Some(b64) = repo.blobs.get(&e.blob_id) else {
return Vec::new();
};
base64::engine::general_purpose::STANDARD
.decode(b64.as_bytes())
.unwrap_or_else(|_| b64.clone().into_bytes())
}
pub(crate) fn hunk_side(bytes: &[u8]) -> Value {
let lines = line_count(bytes);
json!({
"startLine": if lines == 0 { 0 } else { 1 },
"endLine": lines,
"hunkContent": base64::engine::general_purpose::STANDARD.encode(bytes),
})
}
pub(crate) fn merge_operation(
base: Option<&FileEntry>,
side: Option<&FileEntry>,
) -> Option<&'static str> {
match (base, side) {
(None, Some(_)) => Some("A"),
(Some(_), None) => Some("D"),
(Some(_), Some(_)) if !entries_equal(base, side) => Some("M"),
_ => None,
}
}
pub(crate) fn conflict_metadata(
repo: &Repo,
source: &str,
dest: &str,
base_tree: &std::collections::BTreeMap<String, FileEntry>,
src_tree: &std::collections::BTreeMap<String, FileEntry>,
dst_tree: &std::collections::BTreeMap<String, FileEntry>,
path: &str,
) -> (Value, Vec<Value>) {
let _ = (source, dest);
let b = base_tree.get(path);
let s = src_tree.get(path);
let d = dst_tree.get(path);
let sb = entry_bytes(repo, s);
let db = entry_bytes(repo, d);
let bb = entry_bytes(repo, b);
let is_conflict = matches!(merge_entry(b, s, d), Resolved::Conflict);
let content_conflict = is_conflict;
let file_mode_conflict = match (b, s, d) {
(base, Some(se), Some(de)) => {
se.mode != de.mode
&& base.map(|e| e.mode.as_str()) != Some(se.mode.as_str())
&& base.map(|e| e.mode.as_str()) != Some(de.mode.as_str())
}
_ => false,
};
let mode_of = |e: Option<&FileEntry>| e.map(|x| git_mode_to_file(&x.mode)).unwrap_or("NORMAL");
let mut merge_operations = Map::new();
if let Some(o) = merge_operation(b, s) {
merge_operations.insert("source".into(), json!(o));
}
if let Some(o) = merge_operation(b, d) {
merge_operations.insert("destination".into(), json!(o));
}
let number_of_conflicts = i64::from(is_conflict);
let metadata = json!({
"filePath": path,
"fileSizes": {
"source": sb.len(),
"destination": db.len(),
"base": bb.len(),
},
"fileModes": {
"source": mode_of(s),
"destination": mode_of(d),
"base": mode_of(b),
},
"objectTypes": {
"source": if s.is_some() { "FILE" } else { "" },
"destination": if d.is_some() { "FILE" } else { "" },
"base": if b.is_some() { "FILE" } else { "" },
},
"numberOfConflicts": number_of_conflicts,
"isBinaryFile": {
"source": is_binary(&sb),
"destination": is_binary(&db),
"base": is_binary(&bb),
},
"contentConflict": content_conflict,
"fileModeConflict": file_mode_conflict,
"objectTypeConflict": false,
"mergeOperations": Value::Object(merge_operations),
});
let mut hunks = Vec::new();
if !(entries_equal(s, d) && entries_equal(s, b)) {
hunks.push(json!({
"isConflict": is_conflict,
"source": hunk_side(&sb),
"destination": hunk_side(&db),
"base": hunk_side(&bb),
}));
}
(metadata, hunks)
}
pub(crate) fn is_binary(bytes: &[u8]) -> bool {
bytes.contains(&0)
}
pub(crate) fn pr_event(
pr_id: &str,
event_type: &str,
actor: &str,
date: DateTime<Utc>,
metadata: Value,
) -> Value {
let mut e = Map::new();
e.insert("pullRequestId".into(), json!(pr_id));
e.insert("eventDate".into(), ts(date));
e.insert("pullRequestEventType".into(), json!(event_type));
e.insert("actorArn".into(), json!(actor));
if let Some(obj) = metadata.as_object() {
if !obj.is_empty() && event_type == "PULL_REQUEST_CREATED" {
e.insert("pullRequestCreatedEventMetadata".into(), metadata);
}
}
Value::Object(e)
}
pub(crate) fn comment_value(
comment_id: &str,
content: &str,
author: &str,
date: DateTime<Utc>,
in_reply_to: Option<&str>,
) -> Value {
let mut c = Map::new();
c.insert("commentId".into(), json!(comment_id));
c.insert("content".into(), json!(content));
if let Some(r) = in_reply_to {
c.insert("inReplyTo".into(), json!(r));
}
c.insert("creationDate".into(), ts(date));
c.insert("lastModifiedDate".into(), ts(date));
c.insert("authorArn".into(), json!(author));
c.insert("deleted".into(), json!(false));
c.insert("callerReactions".into(), json!([]));
c.insert("reactionCounts".into(), json!({}));
Value::Object(c)
}