use crate::asset::config::{
ASSETS_CONFIG_FILENAME_JSON, AssetConfig, AssetSourceDirectoryConfiguration,
};
use crate::batch_upload::operations::BATCH_UPLOAD_API_VERSION;
use crate::batch_upload::plumbing::ChunkUploader;
use crate::batch_upload::plumbing::Mode::{ByProposal, NormalDeploy};
use crate::batch_upload::{
self,
operations::AssetDeletionReason,
plumbing::{AssetDescriptor, make_project_assets},
};
use crate::canister_api::methods::batch::{compute_evidence, propose_commit_batch};
use crate::canister_api::methods::{
api_version::api_version,
asset_properties::get_assets_properties,
batch::{commit_batch, create_batch},
list::list_assets,
};
use crate::canister_api::types::batch_upload::v0;
use crate::canister_api::types::batch_upload::v1::BatchOperationKind;
use crate::canister_api::types::batch_upload::{
common::ComputeEvidenceArguments, v1::CommitBatchArguments,
};
use crate::error::CompatibilityError::DowngradeV1TOV0Failed;
use crate::error::GatherAssetDescriptorsError;
use crate::error::GatherAssetDescriptorsError::{
DuplicateAssetKey, InvalidDirectoryEntry, InvalidSourceDirectory, LoadConfigFailed,
};
use crate::error::PrepareSyncForProposalError;
use crate::error::SyncError;
use crate::error::SyncError::{ApiVersionQueryFailed, CommitBatchFailed};
use crate::error::UploadContentError;
use crate::error::UploadContentError::{CreateBatchFailed, ListAssetsFailed};
use crate::progress::{AssetSyncProgressRenderer, AssetSyncState};
use candid::Nat;
use ic_agent::AgentError;
use ic_utils::Canister;
use itertools::Itertools;
use serde_bytes::ByteBuf;
use slog::{Logger, debug, info, trace, warn};
use std::collections::HashMap;
use std::path::Path;
use walkdir::WalkDir;
const KNOWN_DIRECTORIES: [&str; 1] = [".well-known"];
pub async fn upload_content_and_assemble_sync_operations(
canister: &Canister<'_>,
canister_api_version: u16,
dirs: &[&Path],
no_delete: bool,
mode: batch_upload::plumbing::Mode,
logger: &Logger,
progress: Option<&dyn AssetSyncProgressRenderer>,
) -> Result<CommitBatchArguments, UploadContentError> {
if let Some(progress) = progress {
progress.set_state(AssetSyncState::GatherAssetDescriptors);
}
let asset_descriptors = gather_asset_descriptors(dirs, logger)?;
if let Some(progress) = progress {
progress.set_state(AssetSyncState::ListAssets);
}
let canister_assets = list_assets(canister).await.map_err(ListAssetsFailed)?;
debug!(
logger,
"Fetching properties for all assets in the canister."
);
let now = std::time::Instant::now();
if let Some(progress) = progress {
progress.set_state(AssetSyncState::GetAssetProperties);
}
let canister_asset_properties =
get_assets_properties(canister, &canister_assets, progress).await?;
debug!(
logger,
"Fetched properties for all assets in the canister in {:?}",
now.elapsed()
);
if let Some(progress) = progress {
progress.set_state(AssetSyncState::CreateBatch);
}
let batch_id = create_batch(canister).await.map_err(CreateBatchFailed)?;
debug!(
logger,
"Staging contents of new and changed assets in batch {}:", batch_id
);
let chunk_uploader =
ChunkUploader::new(canister.clone(), canister_api_version, batch_id.clone());
if let Some(progress) = progress {
progress.set_state(AssetSyncState::StageContents);
}
let project_assets = make_project_assets(
Some(&chunk_uploader),
asset_descriptors,
&canister_assets,
mode,
logger,
progress,
)
.await
.map_err(UploadContentError::CreateProjectAssetError)?;
if let Some(progress) = progress {
progress.set_state(AssetSyncState::AssembleBatch);
}
let commit_batch_args = batch_upload::operations::assemble_commit_batch_arguments(
&chunk_uploader,
project_assets,
canister_assets,
match no_delete {
true => AssetDeletionReason::Incompatible,
false => AssetDeletionReason::Obsolete,
},
canister_asset_properties,
batch_id,
)
.await
.map_err(UploadContentError::AssembleCommitBatchArgumentFailed)?;
debug!(
logger,
"Count of each Batch Operation Kind: {:?}",
commit_batch_args.group_by_kind_then_count()
);
debug!(
logger,
"Chunks: {} Bytes: {}",
chunk_uploader.chunks(),
chunk_uploader.bytes()
);
trace!(logger, "Value of CommitBatch: {:?}", commit_batch_args);
Ok(commit_batch_args)
}
pub async fn sync(
canister: &Canister<'_>,
dirs: &[&Path],
no_delete: bool,
logger: &Logger,
progress: Option<&dyn AssetSyncProgressRenderer>,
) -> Result<(), SyncError> {
let canister_api_version = api_version(canister).await.map_err(ApiVersionQueryFailed)?;
let commit_batch_args = upload_content_and_assemble_sync_operations(
canister,
canister_api_version,
dirs,
no_delete,
NormalDeploy,
logger,
progress,
)
.await?;
debug!(
logger,
"Canister API version: {canister_api_version}. ic-asset API version: {BATCH_UPLOAD_API_VERSION}"
);
debug!(logger, "Committing batch.");
if let Some(progress) = progress {
progress.set_state(AssetSyncState::CommitBatch);
}
match canister_api_version {
0 => {
let commit_batch_args_v0 = v0::CommitBatchArguments::try_from(commit_batch_args).map_err(DowngradeV1TOV0Failed)?;
warn!(logger, "The asset canister is running an old version of the API. It will not be able to set assets properties.");
commit_batch(canister, commit_batch_args_v0).await
}
BATCH_UPLOAD_API_VERSION.. => commit_in_stages(canister, commit_batch_args, logger, progress).await,
}.map_err(CommitBatchFailed)?;
if let Some(progress) = progress {
progress.set_state(AssetSyncState::Done);
}
Ok(())
}
fn create_commit_batches(operations: Vec<BatchOperationKind>) -> Vec<Vec<BatchOperationKind>> {
const MAX_OPERATIONS_PER_BATCH: usize = 500; const MAX_HEADER_MAP_SIZE: usize = 1_500_000;
let mut batches = Vec::new();
let mut current_batch = Vec::new();
let mut operation_count = 0;
let mut header_map_size = 0;
for operation in operations {
let operation_header_size = calculate_header_size(&operation);
let would_exceed_operation_limit = operation_count >= MAX_OPERATIONS_PER_BATCH;
let would_exceed_header_limit =
header_map_size + operation_header_size >= MAX_HEADER_MAP_SIZE;
if (would_exceed_operation_limit || would_exceed_header_limit) && !current_batch.is_empty()
{
batches.push(current_batch);
current_batch = Vec::new();
operation_count = 0;
header_map_size = 0;
}
current_batch.push(operation);
operation_count += 1;
header_map_size += operation_header_size;
}
if !current_batch.is_empty() {
batches.push(current_batch);
}
batches
}
fn calculate_header_size(operation: &BatchOperationKind) -> usize {
match operation {
BatchOperationKind::CreateAsset(args) => args.headers.as_ref().map_or(0, |headers| {
headers.iter().map(|(k, v)| k.len() + v.len()).sum()
}),
BatchOperationKind::SetAssetProperties(args) => args
.headers
.as_ref()
.and_then(|h| h.as_ref())
.map_or(0, |headers| {
headers.iter().map(|(k, v)| k.len() + v.len()).sum()
}),
_ => 0,
}
}
async fn commit_in_stages(
canister: &Canister<'_>,
commit_batch_args: CommitBatchArguments,
logger: &Logger,
progress: Option<&dyn AssetSyncProgressRenderer>,
) -> Result<(), AgentError> {
if let Some(progress) = progress {
progress.set_total_batch_operations(commit_batch_args.operations.len());
}
let batches = create_commit_batches(commit_batch_args.operations);
for operations in batches {
let op_amount = operations.len();
debug!(logger, "Committing batch with {op_amount} operations.");
commit_batch(
canister,
CommitBatchArguments {
batch_id: Nat::from(0_u8),
operations,
},
)
.await?;
if let Some(progress) = progress {
progress.add_committed_batch_operations(op_amount);
}
}
commit_batch(
canister,
CommitBatchArguments {
batch_id: commit_batch_args.batch_id,
operations: vec![],
},
)
.await
}
pub async fn prepare_sync_for_proposal(
canister: &Canister<'_>,
dirs: &[&Path],
logger: &Logger,
progress: Option<&dyn AssetSyncProgressRenderer>,
) -> Result<(Nat, ByteBuf), PrepareSyncForProposalError> {
let canister_api_version = api_version(canister)
.await
.map_err(PrepareSyncForProposalError::ApiVersionQueryFailed)?;
let arg = upload_content_and_assemble_sync_operations(
canister,
canister_api_version,
dirs,
false,
ByProposal,
logger,
progress,
)
.await?;
let arg = sort_batch_operations(arg);
let batch_id = arg.batch_id.clone();
info!(logger, "Preparing batch {}.", batch_id);
propose_commit_batch(canister, arg)
.await
.map_err(PrepareSyncForProposalError::ProposeCommitBatch)?;
let compute_evidence_arg = ComputeEvidenceArguments {
batch_id: batch_id.clone(),
max_iterations: Some(97), };
info!(logger, "Computing evidence.");
let evidence = loop {
if let Some(evidence) = compute_evidence(canister, &compute_evidence_arg)
.await
.map_err(PrepareSyncForProposalError::ComputeEvidence)?
{
break evidence;
}
};
info!(
logger,
"Proposed commit of batch {} with evidence {}. Either commit it by proposal, or delete it.",
batch_id,
hex::encode(&evidence)
);
Ok((batch_id, evidence))
}
fn sort_batch_operations(mut args: CommitBatchArguments) -> CommitBatchArguments {
args.operations.sort();
args
}
fn include_entry(entry: &walkdir::DirEntry, config: &AssetConfig) -> bool {
if let Some(ignored) = config.ignore {
!ignored
} else if let Some(entry_name) = entry.file_name().to_str() {
let is_known = if entry.path().is_dir() {
KNOWN_DIRECTORIES.contains(&entry_name)
} else {
false
};
is_known || !entry_name.starts_with('.')
} else {
true
}
}
pub(crate) fn gather_asset_descriptors(
dirs: &[&Path],
logger: &Logger,
) -> Result<Vec<AssetDescriptor>, GatherAssetDescriptorsError> {
let mut asset_descriptors: HashMap<String, AssetDescriptor> = HashMap::new();
for dir in dirs {
let dir = crate::fs::canonicalize(dir).map_err(InvalidSourceDirectory)?;
let mut configuration =
AssetSourceDirectoryConfiguration::load(&dir).map_err(LoadConfigFailed)?;
let mut asset_descriptors_interim = vec![];
let entries = WalkDir::new(&dir)
.into_iter()
.filter_entry(|entry| {
if let Ok(canonical_path) = &crate::fs::canonicalize(entry.path()) {
let config = configuration
.get_asset_config(canonical_path)
.unwrap_or_default();
include_entry(entry, &config)
} else {
false
}
})
.filter_map(|r| r.ok())
.filter(|entry| {
entry.file_type().is_file() && entry.file_name() != ASSETS_CONFIG_FILENAME_JSON
})
.collect::<Vec<_>>();
for e in entries {
let source = crate::fs::canonicalize(e.path()).map_err(InvalidDirectoryEntry)?;
let relative = source.strip_prefix(&dir).expect("cannot strip prefix");
let key = String::from("/") + relative.to_string_lossy().as_ref();
let config = configuration.get_asset_config(&source)?;
asset_descriptors_interim.push(AssetDescriptor {
source,
key,
config,
})
}
for asset_descriptor in asset_descriptors_interim {
if let Some(already_seen) = asset_descriptors.get(&asset_descriptor.key) {
return Err(DuplicateAssetKey(
asset_descriptor.key.clone(),
Box::new(asset_descriptor.source.clone()),
Box::new(already_seen.source.clone()),
));
}
asset_descriptors.insert(asset_descriptor.key.clone(), asset_descriptor);
}
for (config_path, rules) in configuration.get_unused_configs() {
warn!(
logger,
"{count} unmatched configuration{s} in {path}/.ic-assets.json config file:",
count = rules.len(),
s = if rules.len() > 1 { "s" } else { "" },
path = config_path.display()
);
for rule in rules {
warn!(logger, "{}", serde_json::to_string_pretty(&rule).unwrap());
}
}
let no_policy_assets = asset_descriptors
.values()
.filter(|asset| asset.config.warn_about_no_security_policy())
.collect_vec();
if !no_policy_assets.is_empty() {
let qnt = if no_policy_assets.len() == asset_descriptors.len() {
"any"
} else {
"some"
};
warn!(
logger,
"This project does not define a security policy for {qnt} assets."
);
warn!(
logger,
"You should define a security policy in .ic-assets.json5. For example:"
);
warn!(logger, "[");
warn!(logger, " {{");
warn!(logger, r#" "match": "**/*","#);
warn!(logger, r#" "security_policy": "standard""#);
warn!(logger, " }}");
warn!(logger, "]");
if no_policy_assets.len() != asset_descriptors.len() {
warn!(logger, "Assets without any security policy:");
for asset in &no_policy_assets {
warn!(logger, " - {}", asset.key);
}
}
}
let standard_policy_assets = asset_descriptors
.values()
.filter(|asset| asset.config.warn_about_standard_security_policy())
.collect_vec();
if !standard_policy_assets.is_empty() {
let qnt = if standard_policy_assets.len() == asset_descriptors.len() {
"all"
} else {
"some"
};
warn!(
logger,
"This project uses the default security policy for {qnt} assets. While it is set up to work with many applications, it is recommended to further harden the policy to increase security against attacks like XSS."
);
warn!(
logger,
"To get started, have a look at 'dfx info security-policy'. It shows the default security policy along with suggestions on how to improve it."
);
if standard_policy_assets.len() != asset_descriptors.len() {
warn!(logger, "Unhardened assets:");
for asset in &standard_policy_assets {
warn!(logger, " - {}", asset.key);
}
}
}
if !standard_policy_assets.is_empty() || !no_policy_assets.is_empty() {
warn!(
logger,
"To disable the policy warning, define \"disable_security_policy_warning\": true in .ic-assets.json5."
);
}
let missing_hardening_assets = asset_descriptors
.values()
.filter(|asset| asset.config.warn_about_missing_hardening_headers())
.collect_vec();
if !missing_hardening_assets.is_empty() {
let mut error = String::new();
if missing_hardening_assets.len() == asset_descriptors.len() {
error.push_str("Unhardened assets: all");
} else {
error.push_str("Unhardened assets:");
for asset in &missing_hardening_assets {
error.push_str(&format!("\n - {}", asset.key));
}
}
return Err(GatherAssetDescriptorsError::HardenedSecurityPolicyIsNotHardened(error));
}
}
Ok(asset_descriptors.into_values().collect())
}
#[cfg(test)]
mod test_gathering_asset_descriptors_with_tempdir {
use crate::asset::config::{CacheConfig, HeadersConfig};
use super::AssetDescriptor;
use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
};
use tempfile::{Builder, TempDir};
fn gather_asset_descriptors(dirs: &[&Path]) -> Vec<AssetDescriptor> {
let logger = slog::Logger::root(slog::Discard, slog::o!());
super::gather_asset_descriptors(dirs, &logger).unwrap()
}
impl AssetDescriptor {
fn default_from_path(assets_dir: &Path, relative_path: &str) -> Self {
let relative_path = relative_path.split('/').collect::<Vec<_>>();
let relative_path = relative_path
.iter()
.fold(PathBuf::new(), |acc, x| acc.join(x));
AssetDescriptor {
source: assets_dir.join(&relative_path),
key: format!("/{}", relative_path.to_str().unwrap()),
config: Default::default(),
}
}
fn with_headers(mut self, headers: HashMap<&str, &str>) -> Self {
let headers = headers
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<HeadersConfig>();
let mut h = self.config.headers.unwrap_or_default();
h.extend(headers);
self.config.headers = Some(h);
self
}
fn with_cache(mut self, cache: CacheConfig) -> Self {
self.config.cache = Some(cache);
self
}
}
impl PartialEq for AssetDescriptor {
fn eq(&self, other: &Self) -> bool {
[
self.source == other.source,
self.key == other.key,
self.config.cache == other.config.cache,
self.config.headers == other.config.headers,
self.config.ignore.unwrap_or(false) == other.config.ignore.unwrap_or(false),
]
.into_iter()
.all(|v| v)
}
}
fn create_temporary_assets_directory(modified_files: HashMap<PathBuf, String>) -> TempDir {
let assets_tempdir = Builder::new()
.prefix("assets")
.rand_bytes(5)
.tempdir()
.unwrap();
let mut default_files = HashMap::from([
(Path::new(".ic-assets.json").to_path_buf(), "[]".to_string()),
(Path::new(".hfile").to_path_buf(), "".to_string()),
(Path::new("file").to_path_buf(), "".to_string()),
(
Path::new(".hidden-dir/.ic-assets.json").to_path_buf(),
"[]".to_string(),
),
(
Path::new(".hidden-dir/.hfile").to_path_buf(),
"".to_string(),
),
(Path::new(".hidden-dir/file").to_path_buf(), "".to_string()),
(
Path::new(".hidden-dir/.hidden-dir-nested/.ic-assets.json").to_path_buf(),
"[]".to_string(),
),
(
Path::new(".hidden-dir/.hidden-dir-nested/.hfile").to_path_buf(),
"".to_string(),
),
(
Path::new(".hidden-dir/.hidden-dir-nested/file").to_path_buf(),
"".to_string(),
),
(
Path::new(".hidden-dir-flat/.ic-assets.json").to_path_buf(),
"[]".to_string(),
),
(
Path::new(".hidden-dir-flat/.hfile").to_path_buf(),
"".to_string(),
),
(
Path::new(".hidden-dir-flat/file").to_path_buf(),
"".to_string(),
),
]);
default_files.extend(modified_files);
for (k, v) in default_files {
let path = assets_tempdir.path().join(k);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, v).unwrap();
}
assets_tempdir
}
#[test]
fn gather_all_files() {
let files = HashMap::from([(
Path::new(".ic-assets.json").to_path_buf(),
r#"[
{"match": ".*", "ignore": false}
]"#
.to_string(),
)]);
let assets_temp_dir = create_temporary_assets_directory(files);
let assets_dir = assets_temp_dir.path().canonicalize().unwrap();
let mut asset_descriptors = dbg!(gather_asset_descriptors(&[&assets_dir]));
let mut expected_asset_descriptors = vec![
AssetDescriptor::default_from_path(&assets_dir, ".hfile"),
AssetDescriptor::default_from_path(&assets_dir, "file"),
AssetDescriptor::default_from_path(
&assets_dir,
".hidden-dir/.hidden-dir-nested/.hfile",
),
AssetDescriptor::default_from_path(&assets_dir, ".hidden-dir/.hidden-dir-nested/file"),
AssetDescriptor::default_from_path(&assets_dir, ".hidden-dir-flat/.hfile"),
AssetDescriptor::default_from_path(&assets_dir, ".hidden-dir-flat/file"),
AssetDescriptor::default_from_path(&assets_dir, ".hidden-dir/.hfile"),
AssetDescriptor::default_from_path(&assets_dir, ".hidden-dir/file"),
];
expected_asset_descriptors.sort_by_key(|v| v.source.clone());
asset_descriptors.sort_by_key(|v| v.source.clone());
assert_eq!(asset_descriptors, expected_asset_descriptors);
}
#[test]
fn gather_all_nondot_files_from_nondot_dirs() {
let files = HashMap::from([(
Path::new(".ic-assets.json").to_path_buf(),
r#"[
{"match": ".*", "ignore": true}
]"#
.to_string(),
)]);
let assets_temp_dir = create_temporary_assets_directory(files);
let assets_dir = assets_temp_dir.path().canonicalize().unwrap();
let asset_descriptors = gather_asset_descriptors(&[&assets_dir]);
let expected_asset_descriptors =
vec![AssetDescriptor::default_from_path(&assets_dir, "file")];
assert_eq!(asset_descriptors, expected_asset_descriptors);
let files = HashMap::from([(
Path::new(".ic-assets.json").to_path_buf(),
r#"[
{"match": ".*"}
]"#
.to_string(),
)]);
let assets_temp_dir = create_temporary_assets_directory(files);
let assets_dir = assets_temp_dir.path().canonicalize().unwrap();
let asset_descriptors = gather_asset_descriptors(&[&assets_dir]);
let expected_asset_descriptors =
vec![AssetDescriptor::default_from_path(&assets_dir, "file")];
assert_eq!(asset_descriptors, expected_asset_descriptors);
let files = HashMap::from([(
Path::new(".ic-assets.json").to_path_buf(),
r#"[
{"match": "*"}
]"#
.to_string(),
)]);
let assets_temp_dir = create_temporary_assets_directory(files);
let assets_dir = assets_temp_dir.path().canonicalize().unwrap();
let asset_descriptors = gather_asset_descriptors(&[&assets_dir]);
let expected_asset_descriptors =
vec![AssetDescriptor::default_from_path(&assets_dir, "file")];
assert_eq!(asset_descriptors, expected_asset_descriptors);
let files = HashMap::from([(
Path::new(".ic-assets.json").to_path_buf(),
r#"[
{"match": "**/*"}
]"#
.to_string(),
)]);
let assets_temp_dir = create_temporary_assets_directory(files);
let assets_dir = assets_temp_dir.path().canonicalize().unwrap();
let asset_descriptors = gather_asset_descriptors(&[&assets_dir]);
let expected_asset_descriptors =
vec![AssetDescriptor::default_from_path(&assets_dir, "file")];
assert_eq!(asset_descriptors, expected_asset_descriptors);
}
#[cfg(target_family = "unix")]
#[test]
fn failed_to_include_hidden_dir() {
let files = HashMap::from([(
Path::new(".hidden-dir/.ic-assets.json").to_path_buf(),
r#"[
{"match": ".", "ignore": false},
{"match": "?", "ignore": false},
{"match": "*", "ignore": false},
{"match": "**", "ignore": false},
{"match": ".?", "ignore": false},
{"match": ".*", "ignore": false},
{"match": ".**", "ignore": false},
{"match": "./*", "ignore": false},
{"match": "./**", "ignore": false},
{"match": "./**/*", "ignore": false},
{"match": "./**/**", "ignore": false},
{"match": "../*", "ignore": false},
{"match": "../.*", "ignore": false},
{"match": "../.**", "ignore": false},
{"match": "../.**/*", "ignore": false},
{"match": ".hfile", "ignore": false},
{"match": "file", "ignore": false},
{"match": "file"}
]"#
.to_string(),
)]);
let assets_temp_dir = create_temporary_assets_directory(files);
let assets_dir = assets_temp_dir.path().canonicalize().unwrap();
let mut asset_descriptors = dbg!(gather_asset_descriptors(&[&assets_dir]));
let mut expected_asset_descriptors =
vec![AssetDescriptor::default_from_path(&assets_dir, "file")];
expected_asset_descriptors.sort_by_key(|v| v.key.clone());
asset_descriptors.sort_by_key(|v| v.key.clone());
assert_eq!(asset_descriptors, expected_asset_descriptors)
}
#[test]
fn configuring_dotfiles_step_by_step() {
let files = HashMap::from([
(
Path::new(".ic-assets.json").to_path_buf(),
r#"[{"match": ".hidden-dir", "ignore": false}]"#.to_string(),
),
(
Path::new(".hidden-dir/.ic-assets.json").to_path_buf(),
r#"[
{"match": ".hidden-dir-nested", "ignore": false},
{"match": ".*", "ignore": false, "headers": {"A": "z"}},
{"match": ".hfile", "headers": {"B": "y"}}
]"#
.to_string(),
),
(
Path::new(".hidden-dir/.hidden-dir-nested/.ic-assets.json").to_path_buf(),
r#"[
{"match": "*", "ignore": false, "headers": {"C": "x"}},
{"match": ".hfile", "headers": {"D": "w"}}
]"#
.to_string(),
),
]);
let assets_temp_dir = create_temporary_assets_directory(files);
let assets_dir = assets_temp_dir.path().canonicalize().unwrap();
let mut asset_descriptors = dbg!(gather_asset_descriptors(&[&assets_dir]));
let mut expected_asset_descriptors = vec![
AssetDescriptor::default_from_path(&assets_dir, "file"),
AssetDescriptor::default_from_path(&assets_dir, ".hidden-dir/.hfile")
.with_headers(HashMap::from([("B", "y"), ("A", "z")])),
AssetDescriptor::default_from_path(&assets_dir, ".hidden-dir/file"),
AssetDescriptor::default_from_path(&assets_dir, ".hidden-dir/.hidden-dir-nested/file")
.with_headers(HashMap::from([("A", "z"), ("C", "x")])),
AssetDescriptor::default_from_path(
&assets_dir,
".hidden-dir/.hidden-dir-nested/.hfile",
)
.with_headers(HashMap::from([("D", "w"), ("A", "z"), ("C", "x")])),
];
expected_asset_descriptors.sort_by_key(|v| v.source.clone());
asset_descriptors.sort_by_key(|v| v.source.clone());
assert_eq!(asset_descriptors, expected_asset_descriptors)
}
#[test]
fn include_only_a_specific_dotfile() {
let files = HashMap::from([
(
Path::new(".ic-assets.json").to_path_buf(),
r#"[
{"match": ".hidden-dir", "ignore": false},
{"match": "file", "ignore": true}
]"#
.to_string(),
),
(
Path::new(".hidden-dir/.ic-assets.json").to_path_buf(),
r#"[
{"match": "file", "ignore": true},
{"match": ".hidden-dir-nested", "ignore": false}
]"#
.to_string(),
),
(
Path::new(".hidden-dir/.hidden-dir-nested/.ic-assets.json").to_path_buf(),
r#"[
{"match": "file", "ignore": true},
{"match": ".hfile", "ignore": false, "headers": {"D": "w"}}
]"#
.to_string(),
),
]);
let assets_temp_dir = create_temporary_assets_directory(files);
let assets_dir = assets_temp_dir.path().canonicalize().unwrap();
let mut asset_descriptors = dbg!(gather_asset_descriptors(&[&assets_dir]));
let mut expected_asset_descriptors = vec![
AssetDescriptor::default_from_path(
&assets_dir,
".hidden-dir/.hidden-dir-nested/.hfile",
)
.with_headers(HashMap::from([("D", "w")])),
];
expected_asset_descriptors.sort_by_key(|v| v.source.clone());
asset_descriptors.sort_by_key(|v| v.source.clone());
assert_eq!(asset_descriptors, expected_asset_descriptors);
}
#[test]
fn include_all_files_except_one() {
let files = HashMap::from([
(
Path::new(".ic-assets.json").to_path_buf(),
r#"[
{"match": ".*", "ignore": false}
]"#
.to_string(),
),
(
Path::new(".hidden-dir/.ic-assets.json").to_path_buf(),
r#"[
{"match": "file", "ignore": true}
]"#
.to_string(),
),
]);
let assets_temp_dir = create_temporary_assets_directory(files);
let assets_dir = assets_temp_dir.path().canonicalize().unwrap();
let mut asset_descriptors = dbg!(gather_asset_descriptors(&[&assets_dir]));
let mut expected_asset_descriptors = vec![
AssetDescriptor::default_from_path(&assets_dir, "file"),
AssetDescriptor::default_from_path(&assets_dir, ".hfile"),
AssetDescriptor::default_from_path(&assets_dir, ".hidden-dir-flat/file"),
AssetDescriptor::default_from_path(&assets_dir, ".hidden-dir-flat/.hfile"),
AssetDescriptor::default_from_path(&assets_dir, ".hidden-dir/.hfile"),
AssetDescriptor::default_from_path(&assets_dir, ".hidden-dir/.hidden-dir-nested/file"),
AssetDescriptor::default_from_path(
&assets_dir,
".hidden-dir/.hidden-dir-nested/.hfile",
),
];
expected_asset_descriptors.sort_by_key(|v| v.source.clone());
asset_descriptors.sort_by_key(|v| v.source.clone());
assert_eq!(asset_descriptors, expected_asset_descriptors);
}
#[test]
fn possible_to_reinclude_previously_ignored_file() {
let files = HashMap::from([
(
Path::new(".ic-assets.json").to_path_buf(),
r#"[
{"match": ".hidden-dir-flat", "ignore": false},
{"match": ".hidden-dir-flat/file", "ignore": true }
]"#
.to_string(),
),
(
Path::new(".hidden-dir-flat/.ic-assets.json").to_path_buf(),
r#"[
{"match": "*", "ignore": false},
{"match": "file", "ignore": false}
]"#
.to_string(),
),
]);
let assets_temp_dir = create_temporary_assets_directory(files);
let assets_dir = assets_temp_dir.path().canonicalize().unwrap();
let mut asset_descriptors = dbg!(gather_asset_descriptors(&[&assets_dir]));
let mut expected_asset_descriptors = vec![
AssetDescriptor::default_from_path(&assets_dir, "file"),
AssetDescriptor::default_from_path(&assets_dir, ".hidden-dir-flat/.hfile"),
AssetDescriptor::default_from_path(&assets_dir, ".hidden-dir-flat/file"),
];
expected_asset_descriptors.sort_by_key(|v| v.source.clone());
asset_descriptors.sort_by_key(|v| v.source.clone());
assert_eq!(asset_descriptors, expected_asset_descriptors);
}
#[test]
fn impossible_to_reinclude_file_from_already_ignored_directory() {
let files = HashMap::from([
(Path::new("dir/file").to_path_buf(), "".to_string()),
(Path::new("anotherdir/file").to_path_buf(), "".to_string()),
(
Path::new("anotherdir/.ic-assets.json").to_path_buf(),
r#"[
{"match": "file", "ignore": false}
]"#
.to_string(),
),
(
Path::new(".ic-assets.json").to_path_buf(),
r#"[
{"match": "anotherdir", "ignore": true}
]"#
.to_string(),
),
]);
let assets_temp_dir = create_temporary_assets_directory(files);
let assets_dir = assets_temp_dir.path().canonicalize().unwrap();
let mut asset_descriptors = dbg!(gather_asset_descriptors(&[&assets_dir]));
let mut expected_asset_descriptors = vec![
AssetDescriptor::default_from_path(&assets_dir, "file"),
AssetDescriptor::default_from_path(&assets_dir, "dir/file"),
];
expected_asset_descriptors.sort_by_key(|v| v.source.clone());
asset_descriptors.sort_by_key(|v| v.source.clone());
assert_eq!(asset_descriptors, expected_asset_descriptors);
}
#[test]
fn known_directories_included_by_default() {
let files = HashMap::from([
(
Path::new(".well-known/ic-domains").to_path_buf(),
"foo.bar.com".to_string(),
),
]);
let assets_temp_dir = create_temporary_assets_directory(files);
let assets_dir = assets_temp_dir.path().canonicalize().unwrap();
let mut asset_descriptors = dbg!(gather_asset_descriptors(&[&assets_dir]));
let mut expected_asset_descriptors = vec![
AssetDescriptor::default_from_path(&assets_dir, "file"),
AssetDescriptor::default_from_path(&assets_dir, ".well-known/ic-domains"),
];
expected_asset_descriptors.sort_by_key(|v| v.source.clone());
asset_descriptors.sort_by_key(|v| v.source.clone());
assert_eq!(asset_descriptors, expected_asset_descriptors);
}
#[test]
fn known_directories_can_be_ignored() {
let files = HashMap::from([
(
Path::new(".well-known/ic-domains").to_path_buf(),
"foo.bar.com".to_string(),
),
(
Path::new(".ic-assets.json").to_path_buf(),
r#"[
{"match": ".well-known", "ignore": true}
]"#
.to_string(),
),
]);
let assets_temp_dir = create_temporary_assets_directory(files);
let assets_dir = assets_temp_dir.path().canonicalize().unwrap();
let mut asset_descriptors = dbg!(gather_asset_descriptors(&[&assets_dir]));
let mut expected_asset_descriptors =
vec![AssetDescriptor::default_from_path(&assets_dir, "file")];
expected_asset_descriptors.sort_by_key(|v| v.source.clone());
asset_descriptors.sort_by_key(|v| v.source.clone());
assert_eq!(asset_descriptors, expected_asset_descriptors);
}
#[test]
fn bonanza() {
let files = HashMap::from([
(Path::new("dir/file").to_path_buf(), "".to_string()),
(
Path::new("dir/.ic-assets.json").to_path_buf(),
r#"[
{"match": "file", "headers": { "Access-Control-Allow-Origin": "null" }}
]"#
.to_string(),
),
(Path::new("anotherdir/file").to_path_buf(), "".to_string()),
(
Path::new("anotherdir/.ic-assets.json").to_path_buf(),
r#"[
{"match": "file", "cache": { "max_age": 42 }, "headers": null }
]"#
.to_string(),
),
(
Path::new(".ic-assets.json").to_path_buf(),
r#"[
{"match": "*", "cache": { "max_age": 11 }, "headers": { "X-Content-Type-Options": "nosniff" } },
{"match": "**/.hfile", "ignore": false, "headers": { "X-Content-Type-Options": "*" }},
{"match": ".hidden-dir-flat", "ignore": false },
{"match": ".hidden-dir", "ignore": false }
]"#
.to_string(),
),
(
Path::new(".hidden-dir-flat/.ic-assets.json").to_path_buf(),
r#"[
{"match": "*", "ignore": false, "headers": {"Cross-Origin-Resource-Policy": "same-origin"}},
{"match": ".hfile", "ignore": true}
]"#
.to_string(),
),
]);
let assets_temp_dir = create_temporary_assets_directory(files);
let assets_dir = assets_temp_dir.path().canonicalize().unwrap();
let mut asset_descriptors = gather_asset_descriptors(&[&assets_dir]);
let mut expected_asset_descriptors = vec![
AssetDescriptor::default_from_path(&assets_dir, ".hfile")
.with_headers(HashMap::from([("X-Content-Type-Options", "*")]))
.with_cache(CacheConfig { max_age: Some(11) }),
AssetDescriptor::default_from_path(&assets_dir, ".hidden-dir/.hfile")
.with_headers(HashMap::from([("X-Content-Type-Options", "*")]))
.with_cache(CacheConfig { max_age: Some(11) }),
AssetDescriptor::default_from_path(&assets_dir, ".hidden-dir/file")
.with_headers(HashMap::from([("X-Content-Type-Options", "nosniff")]))
.with_cache(CacheConfig { max_age: Some(11) }),
AssetDescriptor::default_from_path(&assets_dir, ".hidden-dir-flat/file")
.with_headers(HashMap::from([("X-Content-Type-Options", "nosniff")]))
.with_headers(HashMap::from([(
"Cross-Origin-Resource-Policy",
"same-origin",
)]))
.with_cache(CacheConfig { max_age: Some(11) }),
AssetDescriptor::default_from_path(&assets_dir, "anotherdir/file")
.with_cache(CacheConfig { max_age: Some(42) }),
AssetDescriptor::default_from_path(&assets_dir, "dir/file")
.with_headers(HashMap::from([("X-Content-Type-Options", "nosniff")]))
.with_headers(HashMap::from([("Access-Control-Allow-Origin", "null")]))
.with_cache(CacheConfig { max_age: Some(11) }),
AssetDescriptor::default_from_path(&assets_dir, "file")
.with_cache(CacheConfig { max_age: Some(11) })
.with_headers(HashMap::from([("X-Content-Type-Options", "nosniff")])),
];
expected_asset_descriptors.sort_by_key(|v| v.source.clone());
asset_descriptors.sort_by_key(|v| v.source.clone());
assert_eq!(dbg!(asset_descriptors), expected_asset_descriptors);
}
}