use super::{Definition, Update};
use crate::{Config, ObjectId};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Commands {
#[default]
Refuse,
Run,
}
#[derive(Clone, Debug)]
pub struct InitPlan {
name: Vec<u8>,
path: Vec<u8>,
settings: Vec<(&'static str, Vec<u8>)>,
pub(super) expected: [Values; 4],
}
impl InitPlan {
pub fn name(&self) -> &[u8] {
&self.name
}
pub fn path(&self) -> &[u8] {
&self.path
}
pub fn settings(&self) -> impl ExactSizeIterator<Item = (&'static str, &[u8])> {
self.settings
.iter()
.map(|(key, value)| (*key, value.as_slice()))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum UpdateMethod {
BuiltIn(Update),
Command(Vec<u8>),
}
#[derive(Clone, Debug)]
pub struct UpdatePlan {
name: Vec<u8>,
path: Vec<u8>,
recorded: ObjectId,
url: Vec<u8>,
method: UpdateMethod,
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(super) expected: [Values; 3],
shallow: bool,
}
impl UpdatePlan {
pub fn name(&self) -> &[u8] {
&self.name
}
pub fn path(&self) -> &[u8] {
&self.path
}
pub fn recorded(&self) -> ObjectId {
self.recorded
}
pub fn url(&self) -> &[u8] {
&self.url
}
pub fn shallow(&self) -> bool {
self.shallow
}
pub fn method(&self) -> &UpdateMethod {
&self.method
}
}
impl Definition {
pub fn plan_init(
&self,
config: &Config,
superproject_url: &[u8],
) -> Result<InitPlan, PlanError> {
let path = checked_path(self)?;
if config.value("submodule", Some(self.name()), "url") == Some(None) {
return Err(PlanError::InvalidSetting("url"));
}
let mut settings = Vec::new();
if !active(config, self.name(), &path)? {
settings.push(("active", b"true".to_vec()));
}
if config
.value("submodule", Some(self.name()), "url")
.is_none()
{
let url = self.resolve_url(superproject_url)?;
settings.push(("url", url));
}
if self.update_explicit
&& config
.value("submodule", Some(self.name()), "update")
.is_none()
{
let value = match self.update() {
Update::Checkout => b"checkout".as_slice(),
Update::Rebase => b"rebase",
Update::Merge => b"merge",
Update::None => b"none",
};
settings.push(("update", value.to_vec()));
}
Ok(InitPlan {
name: self.name().to_vec(),
path,
settings,
expected: init_values(config, self.name()),
})
}
pub fn plan_update(
&self,
config: &Config,
recorded: ObjectId,
commands: Commands,
) -> Result<UpdatePlan, PlanError> {
let path = checked_path(self)?;
let url = match config.value("submodule", Some(self.name()), "url") {
None => return Err(PlanError::Uninitialized),
Some(Some(url)) if valid_url(url) => url.to_vec(),
_ => return Err(PlanError::InvalidSetting("url")),
};
let entry = config.entries().iter().rev().find(|entry| {
entry.section.eq_ignore_ascii_case(b"submodule")
&& entry.subsection.as_deref() == Some(self.name())
&& entry.name.eq_ignore_ascii_case(b"update")
});
let method = match entry {
None => UpdateMethod::BuiltIn(self.update()),
Some(entry) => match entry.value.as_deref() {
Some(b"checkout") => UpdateMethod::BuiltIn(Update::Checkout),
Some(b"rebase") => UpdateMethod::BuiltIn(Update::Rebase),
Some(b"merge") => UpdateMethod::BuiltIn(Update::Merge),
Some(b"none") => UpdateMethod::BuiltIn(Update::None),
Some(value) if value.starts_with(b"!") => {
let command = &value[1..];
if command.is_empty() || command.contains(&0) {
return Err(PlanError::InvalidSetting("update"));
}
if entry.origin.is_none() {
return Err(PlanError::UntrustedCommand);
}
if commands == Commands::Refuse {
return Err(PlanError::CommandRefused);
}
UpdateMethod::Command(command.to_vec())
}
_ => return Err(PlanError::InvalidSetting("update")),
},
};
Ok(UpdatePlan {
name: self.name().to_vec(),
path,
recorded,
url,
method,
#[cfg(any(target_os = "linux", target_os = "macos"))]
expected: update_values(config, self.name()),
shallow: config
.boolean("submodule", Some(self.name()), "shallow")
.map_err(|_| PlanError::InvalidSetting("shallow"))?
.unwrap_or(self.shallow() == Some(true)),
})
}
}
fn checked_path(definition: &Definition) -> Result<Vec<u8>, PlanError> {
if !super::safe_relative_path(definition.name()) {
return Err(super::Error::UnsafeName.into());
}
let path = definition
.path()
.filter(|path| super::safe_relative_path(path))
.ok_or(super::Error::UnsafePath)?;
Ok(path.to_vec())
}
pub(super) fn active(config: &Config, name: &[u8], path: &[u8]) -> Result<bool, PlanError> {
if let Some(active) = config
.boolean("submodule", Some(name), "active")
.map_err(|_| PlanError::InvalidSetting("active"))?
{
return Ok(active);
}
let specs: Vec<_> = config.values("submodule", None, "active").collect();
if specs.is_empty() {
return Ok(config.string("submodule", Some(name), "url").is_some());
}
let specs: Option<Vec<_>> = specs.into_iter().collect();
let specs = specs.ok_or(PlanError::InvalidSetting("active"))?;
let paths = crate::pathspec::Pathspec::parse(specs, &crate::pathspec::Context::new())?;
paths.matches(path).map_err(|_| PlanError::ActiveAttributes)
}
fn valid_url(url: &[u8]) -> bool {
!url.is_empty() && !url.starts_with(b"-") && !url.iter().any(u8::is_ascii_control)
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum PlanError {
#[error(transparent)]
Definition(#[from] super::Error),
#[error(transparent)]
Url(#[from] super::UrlError),
#[error(transparent)]
ActivePathspec(#[from] crate::pathspec::ParseError),
#[error("submodule activation requires attribute lookup")]
ActiveAttributes,
#[error("submodule is not initialized")]
Uninitialized,
#[error("invalid submodule {0} setting")]
InvalidSetting(&'static str),
#[error("external submodule update method has no trusted source provenance")]
UntrustedCommand,
#[error("external submodule update commands are refused")]
CommandRefused,
}
type Values = Vec<Option<Vec<u8>>>;
fn values(config: &Config, name: Option<&[u8]>, key: &str) -> Values {
config
.values("submodule", name, key)
.map(|value| value.map(<[u8]>::to_vec))
.collect()
}
pub(super) fn init_values(config: &Config, name: &[u8]) -> [Values; 4] {
[
values(config, Some(name), "url"),
values(config, Some(name), "update"),
values(config, Some(name), "active"),
values(config, None, "active"),
]
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(super) fn update_values(config: &Config, name: &[u8]) -> [Values; 3] {
[
values(config, Some(name), "url"),
values(config, Some(name), "update"),
values(config, Some(name), "shallow"),
]
}