Public release release-ea887c8f56cd
Source commit: ea887c8f56cd53985a1179b13e5f1b85c485f584 Public tree identity: sha256:b96a97aacdbc8fd4fc2ea20d0e1450280d46db3f24aabe1475e5fbe20e05f255
This commit is contained in:
parent
9223c54939
commit
2a0f7ded04
37 changed files with 3145 additions and 628 deletions
|
|
@ -1,18 +1,48 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{de::Error as _, Deserialize, Deserializer, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{Digest, NodeId, TaskInstanceId};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub const MAX_VFS_PATH_BYTES: usize = 4096;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
|
||||
pub struct VfsPath(String);
|
||||
|
||||
impl VfsPath {
|
||||
pub fn new(path: impl Into<String>) -> Result<Self, VfsError> {
|
||||
let path = path.into();
|
||||
if !path.starts_with("/vfs/") {
|
||||
return Err(VfsError::InvalidPath(path));
|
||||
return Err(VfsError::invalid(path, "path must start with /vfs/"));
|
||||
}
|
||||
if path.len() > MAX_VFS_PATH_BYTES {
|
||||
return Err(VfsError::invalid(
|
||||
path,
|
||||
"path exceeds the 4096-byte protocol limit",
|
||||
));
|
||||
}
|
||||
if path.chars().any(char::is_control) {
|
||||
return Err(VfsError::invalid(path, "control characters are forbidden"));
|
||||
}
|
||||
if path.contains('\\') {
|
||||
return Err(VfsError::invalid(path, "backslashes are forbidden"));
|
||||
}
|
||||
let relative = &path["/vfs/".len()..];
|
||||
if relative.is_empty() {
|
||||
return Err(VfsError::invalid(
|
||||
path,
|
||||
"path after /vfs/ must not be empty",
|
||||
));
|
||||
}
|
||||
if relative
|
||||
.split('/')
|
||||
.any(|component| component.is_empty() || matches!(component, "." | ".."))
|
||||
{
|
||||
return Err(VfsError::invalid(
|
||||
path,
|
||||
"empty, '.', and '..' path components are forbidden",
|
||||
));
|
||||
}
|
||||
Ok(Self(path))
|
||||
}
|
||||
|
|
@ -22,6 +52,16 @@ impl VfsPath {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for VfsPath {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let path = String::deserialize(deserializer)?;
|
||||
Self::new(path).map_err(D::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct VfsObject {
|
||||
pub path: VfsPath,
|
||||
|
|
@ -63,12 +103,18 @@ pub enum ReuseDecision {
|
|||
|
||||
#[derive(Clone, Debug, Error, PartialEq, Eq)]
|
||||
pub enum VfsError {
|
||||
#[error("VFS path must start with /vfs/: {0}")]
|
||||
InvalidPath(String),
|
||||
#[error("invalid VFS path {path:?}: {reason}")]
|
||||
InvalidPath { path: String, reason: &'static str },
|
||||
#[error("path is not visible in the published VFS manifest: {0}")]
|
||||
NotVisible(String),
|
||||
}
|
||||
|
||||
impl VfsError {
|
||||
fn invalid(path: String, reason: &'static str) -> Self {
|
||||
Self::InvalidPath { path, reason }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct VfsOverlay {
|
||||
task: TaskInstanceId,
|
||||
|
|
@ -238,4 +284,39 @@ mod tests {
|
|||
|
||||
assert_eq!(overlay.pending_len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vfs_path_rejects_the_complete_hostile_protocol_matrix() {
|
||||
for invalid in [
|
||||
"vfs/artifacts/app".to_owned(),
|
||||
"/vfs/".to_owned(),
|
||||
"/vfs/artifacts//app".to_owned(),
|
||||
"/vfs/artifacts/app/".to_owned(),
|
||||
"/vfs/artifacts/./app".to_owned(),
|
||||
"/vfs/artifacts/../app".to_owned(),
|
||||
"/vfs/artifacts\\app".to_owned(),
|
||||
"/vfs/artifacts/bad\0app".to_owned(),
|
||||
format!("/vfs/{}", "x".repeat(MAX_VFS_PATH_BYTES)),
|
||||
] {
|
||||
assert!(
|
||||
VfsPath::new(&invalid).is_err(),
|
||||
"hostile VFS path unexpectedly passed: {invalid:?}"
|
||||
);
|
||||
assert!(
|
||||
serde_json::from_value::<VfsPath>(serde_json::json!(invalid)).is_err(),
|
||||
"hostile VFS path unexpectedly deserialized"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vfs_path_accepts_the_4096_byte_boundary() {
|
||||
let path = format!("/vfs/{}", "x".repeat(MAX_VFS_PATH_BYTES - "/vfs/".len()));
|
||||
assert_eq!(path.len(), MAX_VFS_PATH_BYTES);
|
||||
assert_eq!(VfsPath::new(&path).unwrap().as_str(), path);
|
||||
|
||||
let overlong = format!("{path}x");
|
||||
assert_eq!(overlong.len(), MAX_VFS_PATH_BYTES + 1);
|
||||
assert!(VfsPath::new(overlong).is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue