style: cargo fmt with the workspace tab policy (hard_tabs)

Whitespace only; the fork has no own rustfmt.toml so the Oak root
policy applies.
This commit is contained in:
2026-08-11 23:06:13 +08:00
parent 507d7e6e8d
commit af5482d774
318 changed files with 132564 additions and 132170 deletions
+305 -305
View File
@@ -8,376 +8,376 @@ use futures::{AsyncRead, io::BufReader};
#[cfg(any(unix, windows))]
fn archive_path_is_normal(filename: &str) -> bool {
Path::new(filename).components().all(|c| {
matches!(
c,
std::path::Component::Normal(_) | std::path::Component::CurDir
)
})
Path::new(filename).components().all(|c| {
matches!(
c,
std::path::Component::Normal(_) | std::path::Component::CurDir
)
})
}
#[cfg(windows)]
pub async fn extract_zip<R: AsyncRead + Unpin>(destination: &Path, reader: R) -> Result<()> {
let mut reader = read::stream::ZipFileReader::new(BufReader::new(reader));
let mut reader = read::stream::ZipFileReader::new(BufReader::new(reader));
let destination = &destination
.canonicalize()
.unwrap_or_else(|_| destination.to_path_buf());
let destination = &destination
.canonicalize()
.unwrap_or_else(|_| destination.to_path_buf());
while let Some(mut item) = reader.next_with_entry().await? {
let entry_reader = item.reader_mut();
let entry = entry_reader.entry();
let filename = entry
.filename()
.as_str()
.context("reading zip entry file name")?;
while let Some(mut item) = reader.next_with_entry().await? {
let entry_reader = item.reader_mut();
let entry = entry_reader.entry();
let filename = entry
.filename()
.as_str()
.context("reading zip entry file name")?;
if !archive_path_is_normal(filename) {
reader = item.skip().await.context("reading next zip entry")?;
continue;
}
if !archive_path_is_normal(filename) {
reader = item.skip().await.context("reading next zip entry")?;
continue;
}
let path = destination.join(filename);
let path = destination.join(filename);
if entry
.dir()
.with_context(|| format!("reading zip entry metadata for path {path:?}"))?
{
std::fs::create_dir_all(&path)
.with_context(|| format!("creating directory {path:?}"))?;
} else {
let parent_dir = path
.parent()
.with_context(|| format!("no parent directory for {path:?}"))?;
std::fs::create_dir_all(parent_dir)
.with_context(|| format!("creating parent directory {parent_dir:?}"))?;
let mut file = smol::fs::File::create(&path)
.await
.with_context(|| format!("creating file {path:?}"))?;
futures::io::copy(entry_reader, &mut file)
.await
.with_context(|| format!("extracting into file {path:?}"))?;
}
if entry
.dir()
.with_context(|| format!("reading zip entry metadata for path {path:?}"))?
{
std::fs::create_dir_all(&path)
.with_context(|| format!("creating directory {path:?}"))?;
} else {
let parent_dir = path
.parent()
.with_context(|| format!("no parent directory for {path:?}"))?;
std::fs::create_dir_all(parent_dir)
.with_context(|| format!("creating parent directory {parent_dir:?}"))?;
let mut file = smol::fs::File::create(&path)
.await
.with_context(|| format!("creating file {path:?}"))?;
futures::io::copy(entry_reader, &mut file)
.await
.with_context(|| format!("extracting into file {path:?}"))?;
}
reader = item.skip().await.context("reading next zip entry")?;
}
reader = item.skip().await.context("reading next zip entry")?;
}
Ok(())
Ok(())
}
#[cfg(unix)]
pub async fn extract_zip<R: AsyncRead + Unpin>(destination: &Path, reader: R) -> Result<()> {
// Unix needs file permissions copied when extracting.
// This is only possible to do when a reader impls `AsyncSeek` and `seek::ZipFileReader` is used.
// `stream::ZipFileReader` also has the `unix_permissions` method, but it will always return `Some(0)`.
//
// A typical `reader` comes from a streaming network response, so cannot be sought right away,
// and reading the entire archive into the memory seems wasteful.
//
// So, save the stream into a temporary file first and then get it read with a seeking reader.
let mut file = async_fs::File::from(tempfile::tempfile().context("creating a temporary file")?);
futures::io::copy(&mut BufReader::new(reader), &mut file)
.await
.context("saving archive contents into the temporary file")?;
extract_seekable_zip(destination, file).await
// Unix needs file permissions copied when extracting.
// This is only possible to do when a reader impls `AsyncSeek` and `seek::ZipFileReader` is used.
// `stream::ZipFileReader` also has the `unix_permissions` method, but it will always return `Some(0)`.
//
// A typical `reader` comes from a streaming network response, so cannot be sought right away,
// and reading the entire archive into the memory seems wasteful.
//
// So, save the stream into a temporary file first and then get it read with a seeking reader.
let mut file = async_fs::File::from(tempfile::tempfile().context("creating a temporary file")?);
futures::io::copy(&mut BufReader::new(reader), &mut file)
.await
.context("saving archive contents into the temporary file")?;
extract_seekable_zip(destination, file).await
}
#[cfg(unix)]
pub async fn extract_seekable_zip<R: AsyncRead + AsyncSeek + Unpin>(
destination: &Path,
reader: R,
destination: &Path,
reader: R,
) -> Result<()> {
let mut reader = read::seek::ZipFileReader::new(BufReader::new(reader))
.await
.context("reading the zip archive")?;
let destination = &destination
.canonicalize()
.unwrap_or_else(|_| destination.to_path_buf());
for (i, entry) in reader.file().entries().to_vec().into_iter().enumerate() {
let filename = entry
.filename()
.as_str()
.context("reading zip entry file name")?;
let mut reader = read::seek::ZipFileReader::new(BufReader::new(reader))
.await
.context("reading the zip archive")?;
let destination = &destination
.canonicalize()
.unwrap_or_else(|_| destination.to_path_buf());
for (i, entry) in reader.file().entries().to_vec().into_iter().enumerate() {
let filename = entry
.filename()
.as_str()
.context("reading zip entry file name")?;
if !archive_path_is_normal(filename) {
continue;
}
if !archive_path_is_normal(filename) {
continue;
}
let path = destination.join(filename);
let path = destination.join(filename);
if entry
.dir()
.with_context(|| format!("reading zip entry metadata for path {path:?}"))?
{
std::fs::create_dir_all(&path)
.with_context(|| format!("creating directory {path:?}"))?;
} else {
let parent_dir = path
.parent()
.with_context(|| format!("no parent directory for {path:?}"))?;
std::fs::create_dir_all(parent_dir)
.with_context(|| format!("creating parent directory {parent_dir:?}"))?;
let mut file = smol::fs::File::create(&path)
.await
.with_context(|| format!("creating file {path:?}"))?;
let mut entry_reader = reader
.reader_with_entry(i)
.await
.with_context(|| format!("reading entry for path {path:?}"))?;
futures::io::copy(&mut entry_reader, &mut file)
.await
.with_context(|| format!("extracting into file {path:?}"))?;
if entry
.dir()
.with_context(|| format!("reading zip entry metadata for path {path:?}"))?
{
std::fs::create_dir_all(&path)
.with_context(|| format!("creating directory {path:?}"))?;
} else {
let parent_dir = path
.parent()
.with_context(|| format!("no parent directory for {path:?}"))?;
std::fs::create_dir_all(parent_dir)
.with_context(|| format!("creating parent directory {parent_dir:?}"))?;
let mut file = smol::fs::File::create(&path)
.await
.with_context(|| format!("creating file {path:?}"))?;
let mut entry_reader = reader
.reader_with_entry(i)
.await
.with_context(|| format!("reading entry for path {path:?}"))?;
futures::io::copy(&mut entry_reader, &mut file)
.await
.with_context(|| format!("extracting into file {path:?}"))?;
if let Some(perms) = entry.unix_permissions()
&& perms != 0o000
{
use std::os::unix::fs::PermissionsExt;
let permissions = std::fs::Permissions::from_mode(u32::from(perms));
file.set_permissions(permissions)
.await
.with_context(|| format!("setting permissions for file {path:?}"))?;
}
}
}
if let Some(perms) = entry.unix_permissions()
&& perms != 0o000
{
use std::os::unix::fs::PermissionsExt;
let permissions = std::fs::Permissions::from_mode(u32::from(perms));
file.set_permissions(permissions)
.await
.with_context(|| format!("setting permissions for file {path:?}"))?;
}
}
}
Ok(())
Ok(())
}
#[cfg(test)]
mod tests {
use async_zip::ZipEntryBuilder;
use async_zip::base::write::ZipFileWriter;
use futures::{AsyncSeek, AsyncWriteExt};
use smol::io::Cursor;
use tempfile::TempDir;
use async_zip::ZipEntryBuilder;
use async_zip::base::write::ZipFileWriter;
use futures::{AsyncSeek, AsyncWriteExt};
use smol::io::Cursor;
use tempfile::TempDir;
use super::*;
use super::*;
#[allow(unused_variables)]
async fn compress_zip(src_dir: &Path, dst: &Path, keep_file_permissions: bool) -> Result<()> {
let mut out = smol::fs::File::create(dst).await?;
let mut writer = ZipFileWriter::new(&mut out);
#[allow(unused_variables)]
async fn compress_zip(src_dir: &Path, dst: &Path, keep_file_permissions: bool) -> Result<()> {
let mut out = smol::fs::File::create(dst).await?;
let mut writer = ZipFileWriter::new(&mut out);
for entry in walkdir::WalkDir::new(src_dir) {
let entry = entry?;
let path = entry.path();
for entry in walkdir::WalkDir::new(src_dir) {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
continue;
}
if path.is_dir() {
continue;
}
let relative_path = path.strip_prefix(src_dir)?;
let data = smol::fs::read(&path).await?;
let relative_path = path.strip_prefix(src_dir)?;
let data = smol::fs::read(&path).await?;
let filename = relative_path.display().to_string();
let filename = relative_path.display().to_string();
#[cfg(unix)]
{
let mut builder =
ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate);
use std::os::unix::fs::PermissionsExt;
let metadata = std::fs::metadata(path)?;
let perms = keep_file_permissions.then(|| metadata.permissions().mode() as u16);
builder = builder.unix_permissions(perms.unwrap_or_default());
writer.write_entry_whole(builder, &data).await?;
}
#[cfg(not(unix))]
{
let builder =
ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate);
writer.write_entry_whole(builder, &data).await?;
}
}
#[cfg(unix)]
{
let mut builder =
ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate);
use std::os::unix::fs::PermissionsExt;
let metadata = std::fs::metadata(path)?;
let perms = keep_file_permissions.then(|| metadata.permissions().mode() as u16);
builder = builder.unix_permissions(perms.unwrap_or_default());
writer.write_entry_whole(builder, &data).await?;
}
#[cfg(not(unix))]
{
let builder =
ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate);
writer.write_entry_whole(builder, &data).await?;
}
}
writer.close().await?;
out.flush().await?;
out.sync_all().await?;
writer.close().await?;
out.flush().await?;
out.sync_all().await?;
Ok(())
}
Ok(())
}
#[track_caller]
fn assert_file_content(path: &Path, content: &str) {
assert!(path.exists(), "file not found: {:?}", path);
let actual = std::fs::read_to_string(path).unwrap();
assert_eq!(actual, content);
}
#[track_caller]
fn assert_file_content(path: &Path, content: &str) {
assert!(path.exists(), "file not found: {:?}", path);
let actual = std::fs::read_to_string(path).unwrap();
assert_eq!(actual, content);
}
#[track_caller]
fn make_test_data() -> TempDir {
let dir = tempfile::tempdir().unwrap();
let dst = dir.path();
#[track_caller]
fn make_test_data() -> TempDir {
let dir = tempfile::tempdir().unwrap();
let dst = dir.path();
std::fs::write(dst.join("test"), "Hello world.").unwrap();
std::fs::create_dir_all(dst.join("foo/bar")).unwrap();
std::fs::write(dst.join("foo/bar.txt"), "Foo bar.").unwrap();
std::fs::write(dst.join("foo/dar.md"), "Bar dar.").unwrap();
std::fs::write(dst.join("foo/bar/dar你好.txt"), "你好世界").unwrap();
std::fs::write(dst.join("test"), "Hello world.").unwrap();
std::fs::create_dir_all(dst.join("foo/bar")).unwrap();
std::fs::write(dst.join("foo/bar.txt"), "Foo bar.").unwrap();
std::fs::write(dst.join("foo/dar.md"), "Bar dar.").unwrap();
std::fs::write(dst.join("foo/bar/dar你好.txt"), "你好世界").unwrap();
dir
}
dir
}
async fn read_archive(path: &Path) -> impl AsyncRead + AsyncSeek + Unpin {
let data = smol::fs::read(&path).await.unwrap();
Cursor::new(data)
}
async fn read_archive(path: &Path) -> impl AsyncRead + AsyncSeek + Unpin {
let data = smol::fs::read(&path).await.unwrap();
Cursor::new(data)
}
#[test]
fn test_extract_zip() {
let test_dir = make_test_data();
let zip_file = test_dir.path().join("test.zip");
#[test]
fn test_extract_zip() {
let test_dir = make_test_data();
let zip_file = test_dir.path().join("test.zip");
smol::block_on(async {
compress_zip(test_dir.path(), &zip_file, true)
.await
.unwrap();
let reader = read_archive(&zip_file).await;
smol::block_on(async {
compress_zip(test_dir.path(), &zip_file, true)
.await
.unwrap();
let reader = read_archive(&zip_file).await;
let dir = tempfile::tempdir().unwrap();
let dst = dir.path();
extract_zip(dst, reader).await.unwrap();
let dir = tempfile::tempdir().unwrap();
let dst = dir.path();
extract_zip(dst, reader).await.unwrap();
assert_file_content(&dst.join("test"), "Hello world.");
assert_file_content(&dst.join("foo/bar.txt"), "Foo bar.");
assert_file_content(&dst.join("foo/dar.md"), "Bar dar.");
assert_file_content(&dst.join("foo/bar/dar你好.txt"), "你好世界");
});
}
assert_file_content(&dst.join("test"), "Hello world.");
assert_file_content(&dst.join("foo/bar.txt"), "Foo bar.");
assert_file_content(&dst.join("foo/dar.md"), "Bar dar.");
assert_file_content(&dst.join("foo/bar/dar你好.txt"), "你好世界");
});
}
#[cfg(unix)]
#[test]
fn test_extract_zip_preserves_executable_permissions() {
use std::os::unix::fs::PermissionsExt;
#[cfg(unix)]
#[test]
fn test_extract_zip_preserves_executable_permissions() {
use std::os::unix::fs::PermissionsExt;
smol::block_on(async {
let test_dir = tempfile::tempdir().unwrap();
let executable_path = test_dir.path().join("my_script");
smol::block_on(async {
let test_dir = tempfile::tempdir().unwrap();
let executable_path = test_dir.path().join("my_script");
// Create an executable file
std::fs::write(&executable_path, "#!/bin/bash\necho 'Hello'").unwrap();
let mut perms = std::fs::metadata(&executable_path).unwrap().permissions();
perms.set_mode(0o755); // rwxr-xr-x
std::fs::set_permissions(&executable_path, perms).unwrap();
// Create an executable file
std::fs::write(&executable_path, "#!/bin/bash\necho 'Hello'").unwrap();
let mut perms = std::fs::metadata(&executable_path).unwrap().permissions();
perms.set_mode(0o755); // rwxr-xr-x
std::fs::set_permissions(&executable_path, perms).unwrap();
// Create zip
let zip_file = test_dir.path().join("test.zip");
compress_zip(test_dir.path(), &zip_file, true)
.await
.unwrap();
// Create zip
let zip_file = test_dir.path().join("test.zip");
compress_zip(test_dir.path(), &zip_file, true)
.await
.unwrap();
// Extract to new location
let extract_dir = tempfile::tempdir().unwrap();
let reader = read_archive(&zip_file).await;
extract_zip(extract_dir.path(), reader).await.unwrap();
// Extract to new location
let extract_dir = tempfile::tempdir().unwrap();
let reader = read_archive(&zip_file).await;
extract_zip(extract_dir.path(), reader).await.unwrap();
// Check permissions are preserved
let extracted_path = extract_dir.path().join("my_script");
assert!(extracted_path.exists());
let extracted_perms = std::fs::metadata(&extracted_path).unwrap().permissions();
assert_eq!(extracted_perms.mode() & 0o777, 0o755);
});
}
// Check permissions are preserved
let extracted_path = extract_dir.path().join("my_script");
assert!(extracted_path.exists());
let extracted_perms = std::fs::metadata(&extracted_path).unwrap().permissions();
assert_eq!(extracted_perms.mode() & 0o777, 0o755);
});
}
#[cfg(unix)]
#[test]
fn test_extract_zip_sets_default_permissions() {
use std::os::unix::fs::PermissionsExt;
#[cfg(unix)]
#[test]
fn test_extract_zip_sets_default_permissions() {
use std::os::unix::fs::PermissionsExt;
smol::block_on(async {
let test_dir = tempfile::tempdir().unwrap();
let file_path = test_dir.path().join("my_script");
smol::block_on(async {
let test_dir = tempfile::tempdir().unwrap();
let file_path = test_dir.path().join("my_script");
std::fs::write(&file_path, "#!/bin/bash\necho 'Hello'").unwrap();
// The permissions will be shaped by the umask in the test environment
let original_perms = std::fs::metadata(&file_path).unwrap().permissions();
std::fs::write(&file_path, "#!/bin/bash\necho 'Hello'").unwrap();
// The permissions will be shaped by the umask in the test environment
let original_perms = std::fs::metadata(&file_path).unwrap().permissions();
// Create zip
let zip_file = test_dir.path().join("test.zip");
compress_zip(test_dir.path(), &zip_file, false)
.await
.unwrap();
// Create zip
let zip_file = test_dir.path().join("test.zip");
compress_zip(test_dir.path(), &zip_file, false)
.await
.unwrap();
// Extract to new location
let extract_dir = tempfile::tempdir().unwrap();
let reader = read_archive(&zip_file).await;
extract_zip(extract_dir.path(), reader).await.unwrap();
// Extract to new location
let extract_dir = tempfile::tempdir().unwrap();
let reader = read_archive(&zip_file).await;
extract_zip(extract_dir.path(), reader).await.unwrap();
// Permissions were not stored, so will be whatever the umask generates
// by default for new files. This should match what we saw when we previously wrote
// the file.
let extracted_path = extract_dir.path().join("my_script");
assert!(extracted_path.exists());
let extracted_perms = std::fs::metadata(&extracted_path).unwrap().permissions();
assert_eq!(
extracted_perms.mode(),
original_perms.mode(),
"Expected matching Unix file mode for unzipped file without keep_file_permissions"
);
assert_eq!(
extracted_perms, original_perms,
"Expected default set of permissions for unzipped file without keep_file_permissions"
);
});
}
// Permissions were not stored, so will be whatever the umask generates
// by default for new files. This should match what we saw when we previously wrote
// the file.
let extracted_path = extract_dir.path().join("my_script");
assert!(extracted_path.exists());
let extracted_perms = std::fs::metadata(&extracted_path).unwrap().permissions();
assert_eq!(
extracted_perms.mode(),
original_perms.mode(),
"Expected matching Unix file mode for unzipped file without keep_file_permissions"
);
assert_eq!(
extracted_perms, original_perms,
"Expected default set of permissions for unzipped file without keep_file_permissions"
);
});
}
#[test]
fn test_archive_path_is_normal_rejects_traversal() {
assert!(!archive_path_is_normal("../parent.txt"));
assert!(!archive_path_is_normal("foo/../../grandparent.txt"));
assert!(!archive_path_is_normal("/tmp/absolute.txt"));
#[test]
fn test_archive_path_is_normal_rejects_traversal() {
assert!(!archive_path_is_normal("../parent.txt"));
assert!(!archive_path_is_normal("foo/../../grandparent.txt"));
assert!(!archive_path_is_normal("/tmp/absolute.txt"));
assert!(archive_path_is_normal("foo/bar.txt"));
assert!(archive_path_is_normal("foo/bar/baz.txt"));
assert!(archive_path_is_normal("./foo/bar.txt"));
assert!(archive_path_is_normal("normal.txt"));
}
assert!(archive_path_is_normal("foo/bar.txt"));
assert!(archive_path_is_normal("foo/bar/baz.txt"));
assert!(archive_path_is_normal("./foo/bar.txt"));
assert!(archive_path_is_normal("normal.txt"));
}
async fn build_zip_with_entries(entries: &[(&str, &[u8])]) -> Cursor<Vec<u8>> {
let mut buf = Cursor::new(Vec::new());
let mut writer = ZipFileWriter::new(&mut buf);
for (name, data) in entries {
let builder = ZipEntryBuilder::new((*name).into(), async_zip::Compression::Stored);
writer.write_entry_whole(builder, data).await.unwrap();
}
writer.close().await.unwrap();
buf.set_position(0);
buf
}
async fn build_zip_with_entries(entries: &[(&str, &[u8])]) -> Cursor<Vec<u8>> {
let mut buf = Cursor::new(Vec::new());
let mut writer = ZipFileWriter::new(&mut buf);
for (name, data) in entries {
let builder = ZipEntryBuilder::new((*name).into(), async_zip::Compression::Stored);
writer.write_entry_whole(builder, data).await.unwrap();
}
writer.close().await.unwrap();
buf.set_position(0);
buf
}
#[test]
fn test_extract_zip_skips_path_traversal_entries() {
smol::block_on(async {
let base_dir = tempfile::tempdir().unwrap();
let extract_dir = base_dir.path().join("subdir");
std::fs::create_dir_all(&extract_dir).unwrap();
#[test]
fn test_extract_zip_skips_path_traversal_entries() {
smol::block_on(async {
let base_dir = tempfile::tempdir().unwrap();
let extract_dir = base_dir.path().join("subdir");
std::fs::create_dir_all(&extract_dir).unwrap();
let absolute_target = base_dir.path().join("absolute.txt");
let reader = build_zip_with_entries(&[
("normal.txt", b"normal file"),
("subdir/nested.txt", b"nested file"),
("../parent.txt", b"parent file"),
("foo/../../grandparent.txt", b"grandparent file"),
(absolute_target.to_str().unwrap(), b"absolute file"),
])
.await;
let absolute_target = base_dir.path().join("absolute.txt");
let reader = build_zip_with_entries(&[
("normal.txt", b"normal file"),
("subdir/nested.txt", b"nested file"),
("../parent.txt", b"parent file"),
("foo/../../grandparent.txt", b"grandparent file"),
(absolute_target.to_str().unwrap(), b"absolute file"),
])
.await;
extract_zip(&extract_dir, reader).await.unwrap();
extract_zip(&extract_dir, reader).await.unwrap();
assert_file_content(&extract_dir.join("normal.txt"), "normal file");
assert_file_content(&extract_dir.join("subdir/nested.txt"), "nested file");
assert_file_content(&extract_dir.join("normal.txt"), "normal file");
assert_file_content(&extract_dir.join("subdir/nested.txt"), "nested file");
assert!(
!base_dir.path().join("parent.txt").exists(),
"parent traversal entry should have been skipped"
);
assert!(
!base_dir.path().join("grandparent.txt").exists(),
"nested traversal entry should have been skipped"
);
assert!(
!absolute_target.exists(),
"absolute path entry should have been skipped"
);
});
}
assert!(
!base_dir.path().join("parent.txt").exists(),
"parent traversal entry should have been skipped"
);
assert!(
!base_dir.path().join("grandparent.txt").exists(),
"nested traversal entry should have been skipped"
);
assert!(
!absolute_target.exists(),
"absolute path entry should have been skipped"
);
});
}
}
+86 -86
View File
@@ -12,21 +12,21 @@ pub use darwin::{Child, Command, Stdio};
const CREATE_NO_WINDOW: u32 = 0x0800_0000_u32;
pub fn new_command(program: impl AsRef<OsStr>) -> Command {
Command::new(program)
Command::new(program)
}
#[cfg(target_os = "windows")]
pub fn new_std_command(program: impl AsRef<OsStr>) -> std::process::Command {
use std::os::windows::process::CommandExt;
use std::os::windows::process::CommandExt;
let mut command = std::process::Command::new(program);
command.creation_flags(CREATE_NO_WINDOW);
command
let mut command = std::process::Command::new(program);
command.creation_flags(CREATE_NO_WINDOW);
command
}
#[cfg(not(target_os = "windows"))]
pub fn new_std_command(program: impl AsRef<OsStr>) -> std::process::Command {
std::process::Command::new(program)
std::process::Command::new(program)
}
#[cfg(not(target_os = "macos"))]
@@ -41,100 +41,100 @@ pub struct Command(smol::process::Command);
#[cfg(not(target_os = "macos"))]
impl Command {
#[inline]
pub fn new(program: impl AsRef<OsStr>) -> Self {
#[cfg(target_os = "windows")]
{
use smol::process::windows::CommandExt;
let mut cmd = smol::process::Command::new(program);
cmd.creation_flags(CREATE_NO_WINDOW);
Self(cmd)
}
#[cfg(not(target_os = "windows"))]
Self(smol::process::Command::new(program))
}
#[inline]
pub fn new(program: impl AsRef<OsStr>) -> Self {
#[cfg(target_os = "windows")]
{
use smol::process::windows::CommandExt;
let mut cmd = smol::process::Command::new(program);
cmd.creation_flags(CREATE_NO_WINDOW);
Self(cmd)
}
#[cfg(not(target_os = "windows"))]
Self(smol::process::Command::new(program))
}
pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
self.0.arg(arg);
self
}
pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
self.0.arg(arg);
self
}
pub fn args<I, S>(&mut self, args: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
self.0.args(args);
self
}
pub fn args<I, S>(&mut self, args: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
self.0.args(args);
self
}
pub fn get_args(&self) -> impl Iterator<Item = &OsStr> {
self.0.get_args()
}
pub fn get_args(&self) -> impl Iterator<Item = &OsStr> {
self.0.get_args()
}
pub fn env(&mut self, key: impl AsRef<OsStr>, val: impl AsRef<OsStr>) -> &mut Self {
self.0.env(key, val);
self
}
pub fn env(&mut self, key: impl AsRef<OsStr>, val: impl AsRef<OsStr>) -> &mut Self {
self.0.env(key, val);
self
}
pub fn envs<I, K, V>(&mut self, vars: I) -> &mut Self
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.0.envs(vars);
self
}
pub fn envs<I, K, V>(&mut self, vars: I) -> &mut Self
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.0.envs(vars);
self
}
pub fn env_remove(&mut self, key: impl AsRef<OsStr>) -> &mut Self {
self.0.env_remove(key);
self
}
pub fn env_remove(&mut self, key: impl AsRef<OsStr>) -> &mut Self {
self.0.env_remove(key);
self
}
pub fn env_clear(&mut self) -> &mut Self {
self.0.env_clear();
self
}
pub fn env_clear(&mut self) -> &mut Self {
self.0.env_clear();
self
}
pub fn current_dir(&mut self, dir: impl AsRef<Path>) -> &mut Self {
self.0.current_dir(dir);
self
}
pub fn current_dir(&mut self, dir: impl AsRef<Path>) -> &mut Self {
self.0.current_dir(dir);
self
}
pub fn stdin(&mut self, cfg: impl Into<Stdio>) -> &mut Self {
self.0.stdin(cfg.into());
self
}
pub fn stdin(&mut self, cfg: impl Into<Stdio>) -> &mut Self {
self.0.stdin(cfg.into());
self
}
pub fn stdout(&mut self, cfg: impl Into<Stdio>) -> &mut Self {
self.0.stdout(cfg.into());
self
}
pub fn stdout(&mut self, cfg: impl Into<Stdio>) -> &mut Self {
self.0.stdout(cfg.into());
self
}
pub fn stderr(&mut self, cfg: impl Into<Stdio>) -> &mut Self {
self.0.stderr(cfg.into());
self
}
pub fn stderr(&mut self, cfg: impl Into<Stdio>) -> &mut Self {
self.0.stderr(cfg.into());
self
}
pub fn kill_on_drop(&mut self, kill_on_drop: bool) -> &mut Self {
self.0.kill_on_drop(kill_on_drop);
self
}
pub fn kill_on_drop(&mut self, kill_on_drop: bool) -> &mut Self {
self.0.kill_on_drop(kill_on_drop);
self
}
pub fn spawn(&mut self) -> std::io::Result<Child> {
self.0.spawn()
}
pub fn spawn(&mut self) -> std::io::Result<Child> {
self.0.spawn()
}
pub async fn output(&mut self) -> std::io::Result<std::process::Output> {
self.0.output().await
}
pub async fn output(&mut self) -> std::io::Result<std::process::Output> {
self.0.output().await
}
pub async fn status(&mut self) -> std::io::Result<std::process::ExitStatus> {
self.0.status().await
}
pub async fn status(&mut self) -> std::io::Result<std::process::ExitStatus> {
self.0.status().await
}
pub fn get_program(&self) -> &OsStr {
self.0.get_program()
}
pub fn get_program(&self) -> &OsStr {
self.0.get_program()
}
}
File diff suppressed because it is too large Load Diff
+160 -160
View File
@@ -12,191 +12,191 @@ use std::hash::Hash;
/// an item reaches its fixed point, it is assumed it will no longer change and
/// will no longer be checked for collisions.
pub fn compute_disambiguation_details<T, D>(
items: &[T],
get_description: impl Fn(&T, usize) -> D,
items: &[T],
get_description: impl Fn(&T, usize) -> D,
) -> Vec<usize>
where
D: Eq + Hash + Clone,
D: Eq + Hash + Clone,
{
let mut details = vec![0usize; items.len()];
let mut descriptions: HashMap<D, Vec<usize>> = HashMap::default();
let mut current_descriptions: Vec<D> =
items.iter().map(|item| get_description(item, 0)).collect();
let mut details = vec![0usize; items.len()];
let mut descriptions: HashMap<D, Vec<usize>> = HashMap::default();
let mut current_descriptions: Vec<D> =
items.iter().map(|item| get_description(item, 0)).collect();
loop {
let mut any_collisions = false;
loop {
let mut any_collisions = false;
for (index, (item, &detail)) in items.iter().zip(&details).enumerate() {
if detail > 0 {
let new_description = get_description(item, detail);
if new_description == current_descriptions[index] {
continue;
}
current_descriptions[index] = new_description;
}
descriptions
.entry(current_descriptions[index].clone())
.or_default()
.push(index);
}
for (index, (item, &detail)) in items.iter().zip(&details).enumerate() {
if detail > 0 {
let new_description = get_description(item, detail);
if new_description == current_descriptions[index] {
continue;
}
current_descriptions[index] = new_description;
}
descriptions
.entry(current_descriptions[index].clone())
.or_default()
.push(index);
}
for (_, indices) in descriptions.drain() {
if indices.len() > 1 {
any_collisions = true;
for index in indices {
details[index] += 1;
}
}
}
for (_, indices) in descriptions.drain() {
if indices.len() > 1 {
any_collisions = true;
for index in indices {
details[index] += 1;
}
}
}
if !any_collisions {
break;
}
}
if !any_collisions {
break;
}
}
details
details
}
#[cfg(test)]
mod tests {
use super::*;
use super::*;
#[test]
fn test_no_conflicts() {
let items = vec!["alpha", "beta", "gamma"];
let details = compute_disambiguation_details(&items, |item, _detail| item.to_string());
assert_eq!(details, vec![0, 0, 0]);
}
#[test]
fn test_no_conflicts() {
let items = vec!["alpha", "beta", "gamma"];
let details = compute_disambiguation_details(&items, |item, _detail| item.to_string());
assert_eq!(details, vec![0, 0, 0]);
}
#[test]
fn test_simple_two_way_conflict() {
// Two items with the same base name but different parents.
let items = vec![("src/foo.rs", "foo.rs"), ("lib/foo.rs", "foo.rs")];
let details = compute_disambiguation_details(&items, |item, detail| match detail {
0 => item.1.to_string(),
_ => item.0.to_string(),
});
assert_eq!(details, vec![1, 1]);
}
#[test]
fn test_simple_two_way_conflict() {
// Two items with the same base name but different parents.
let items = vec![("src/foo.rs", "foo.rs"), ("lib/foo.rs", "foo.rs")];
let details = compute_disambiguation_details(&items, |item, detail| match detail {
0 => item.1.to_string(),
_ => item.0.to_string(),
});
assert_eq!(details, vec![1, 1]);
}
#[test]
fn test_three_way_conflict() {
let items = vec![
("foo.rs", "a/foo.rs"),
("foo.rs", "b/foo.rs"),
("foo.rs", "c/foo.rs"),
];
let details = compute_disambiguation_details(&items, |item, detail| match detail {
0 => item.0.to_string(),
_ => item.1.to_string(),
});
assert_eq!(details, vec![1, 1, 1]);
}
#[test]
fn test_three_way_conflict() {
let items = vec![
("foo.rs", "a/foo.rs"),
("foo.rs", "b/foo.rs"),
("foo.rs", "c/foo.rs"),
];
let details = compute_disambiguation_details(&items, |item, detail| match detail {
0 => item.0.to_string(),
_ => item.1.to_string(),
});
assert_eq!(details, vec![1, 1, 1]);
}
#[test]
fn test_deeper_conflict() {
// At detail 0, all three show "file.rs".
// At detail 1, items 0 and 1 both show "src/file.rs", item 2 shows "lib/file.rs".
// At detail 2, item 0 shows "a/src/file.rs", item 1 shows "b/src/file.rs".
let items = vec![
vec!["file.rs", "src/file.rs", "a/src/file.rs"],
vec!["file.rs", "src/file.rs", "b/src/file.rs"],
vec!["file.rs", "lib/file.rs", "x/lib/file.rs"],
];
let details = compute_disambiguation_details(&items, |item, detail| {
let clamped = detail.min(item.len() - 1);
item[clamped].to_string()
});
assert_eq!(details, vec![2, 2, 1]);
}
#[test]
fn test_deeper_conflict() {
// At detail 0, all three show "file.rs".
// At detail 1, items 0 and 1 both show "src/file.rs", item 2 shows "lib/file.rs".
// At detail 2, item 0 shows "a/src/file.rs", item 1 shows "b/src/file.rs".
let items = vec![
vec!["file.rs", "src/file.rs", "a/src/file.rs"],
vec!["file.rs", "src/file.rs", "b/src/file.rs"],
vec!["file.rs", "lib/file.rs", "x/lib/file.rs"],
];
let details = compute_disambiguation_details(&items, |item, detail| {
let clamped = detail.min(item.len() - 1);
item[clamped].to_string()
});
assert_eq!(details, vec![2, 2, 1]);
}
#[test]
fn test_mixed_conflicting_and_unique() {
let items = vec![
("src/foo.rs", "foo.rs"),
("lib/foo.rs", "foo.rs"),
("src/bar.rs", "bar.rs"),
];
let details = compute_disambiguation_details(&items, |item, detail| match detail {
0 => item.1.to_string(),
_ => item.0.to_string(),
});
assert_eq!(details, vec![1, 1, 0]);
}
#[test]
fn test_mixed_conflicting_and_unique() {
let items = vec![
("src/foo.rs", "foo.rs"),
("lib/foo.rs", "foo.rs"),
("src/bar.rs", "bar.rs"),
];
let details = compute_disambiguation_details(&items, |item, detail| match detail {
0 => item.1.to_string(),
_ => item.0.to_string(),
});
assert_eq!(details, vec![1, 1, 0]);
}
#[test]
fn test_identical_items_terminates() {
// All items return the same description at every detail level.
// The algorithm must terminate rather than looping forever.
let items = vec!["same", "same", "same"];
let details = compute_disambiguation_details(&items, |item, _detail| item.to_string());
// After bumping to 1, the description doesn't change from level 0,
// so the items are skipped and the loop terminates.
assert_eq!(details, vec![1, 1, 1]);
}
#[test]
fn test_identical_items_terminates() {
// All items return the same description at every detail level.
// The algorithm must terminate rather than looping forever.
let items = vec!["same", "same", "same"];
let details = compute_disambiguation_details(&items, |item, _detail| item.to_string());
// After bumping to 1, the description doesn't change from level 0,
// so the items are skipped and the loop terminates.
assert_eq!(details, vec![1, 1, 1]);
}
#[test]
fn test_single_item() {
let items = vec!["only"];
let details = compute_disambiguation_details(&items, |item, _detail| item.to_string());
assert_eq!(details, vec![0]);
}
#[test]
fn test_single_item() {
let items = vec!["only"];
let details = compute_disambiguation_details(&items, |item, _detail| item.to_string());
assert_eq!(details, vec![0]);
}
#[test]
fn test_empty_input() {
let items: Vec<&str> = vec![];
let details = compute_disambiguation_details(&items, |item, _detail| item.to_string());
let expected: Vec<usize> = vec![];
assert_eq!(details, expected);
}
#[test]
fn test_empty_input() {
let items: Vec<&str> = vec![];
let details = compute_disambiguation_details(&items, |item, _detail| item.to_string());
let expected: Vec<usize> = vec![];
assert_eq!(details, expected);
}
#[test]
fn test_duplicate_paths_from_multiple_groups() {
use std::path::Path;
#[test]
fn test_duplicate_paths_from_multiple_groups() {
use std::path::Path;
// Simulates the sidebar scenario: a path like /Users/rtfeldman/code/zed
// appears in two project groups (e.g. "zed" alone and "zed, roc").
// After deduplication, only unique paths should be disambiguated.
//
// Paths:
// /Users/rtfeldman/code/worktrees/zed/focal-arrow/zed (group 1)
// /Users/rtfeldman/code/zed (group 2)
// /Users/rtfeldman/code/zed (group 3, same path as group 2)
// /Users/rtfeldman/code/roc (group 3)
//
// A naive flat_map collects duplicates. The duplicate /code/zed entries
// collide with each other and drive the detail to the full path.
// The fix is to deduplicate before disambiguating.
// Simulates the sidebar scenario: a path like /Users/rtfeldman/code/zed
// appears in two project groups (e.g. "zed" alone and "zed, roc").
// After deduplication, only unique paths should be disambiguated.
//
// Paths:
// /Users/rtfeldman/code/worktrees/zed/focal-arrow/zed (group 1)
// /Users/rtfeldman/code/zed (group 2)
// /Users/rtfeldman/code/zed (group 3, same path as group 2)
// /Users/rtfeldman/code/roc (group 3)
//
// A naive flat_map collects duplicates. The duplicate /code/zed entries
// collide with each other and drive the detail to the full path.
// The fix is to deduplicate before disambiguating.
fn path_suffix(path: &Path, detail: usize) -> String {
let mut components: Vec<_> = path
.components()
.rev()
.filter_map(|c| match c {
std::path::Component::Normal(s) => Some(s.to_string_lossy()),
_ => None,
})
.take(detail + 1)
.collect();
components.reverse();
components.join("/")
}
fn path_suffix(path: &Path, detail: usize) -> String {
let mut components: Vec<_> = path
.components()
.rev()
.filter_map(|c| match c {
std::path::Component::Normal(s) => Some(s.to_string_lossy()),
_ => None,
})
.take(detail + 1)
.collect();
components.reverse();
components.join("/")
}
let all_paths: Vec<&Path> = vec![
Path::new("/Users/rtfeldman/code/worktrees/zed/focal-arrow/zed"),
Path::new("/Users/rtfeldman/code/zed"),
Path::new("/Users/rtfeldman/code/roc"),
];
let all_paths: Vec<&Path> = vec![
Path::new("/Users/rtfeldman/code/worktrees/zed/focal-arrow/zed"),
Path::new("/Users/rtfeldman/code/zed"),
Path::new("/Users/rtfeldman/code/roc"),
];
let details =
compute_disambiguation_details(&all_paths, |path, detail| path_suffix(path, detail));
let details =
compute_disambiguation_details(&all_paths, |path, detail| path_suffix(path, detail));
// focal-arrow/zed and code/zed both end in "zed", so they need detail 1.
// "roc" is unique at detail 0.
assert_eq!(details, vec![1, 1, 0]);
// focal-arrow/zed and code/zed both end in "zed", so they need detail 1.
// "roc" is unique at detail 0.
assert_eq!(details, vec![1, 1, 0]);
assert_eq!(path_suffix(all_paths[0], details[0]), "focal-arrow/zed");
assert_eq!(path_suffix(all_paths[1], details[1]), "code/zed");
assert_eq!(path_suffix(all_paths[2], details[2]), "roc");
}
assert_eq!(path_suffix(all_paths[0], details[0]), "focal-arrow/zed");
assert_eq!(path_suffix(all_paths[1], details[1]), "code/zed");
assert_eq!(path_suffix(all_paths[2], details[2]), "roc");
}
}
+65 -65
View File
@@ -7,99 +7,99 @@ use std::path::{Path, PathBuf};
/// Removes all files and directories matching the given predicate
pub async fn remove_matching<F>(dir: &Path, predicate: F)
where
F: Fn(&Path) -> bool,
F: Fn(&Path) -> bool,
{
if let Some(mut entries) = fs::read_dir(dir).await.log_err() {
while let Some(entry) = entries.next().await {
if let Some(entry) = entry.log_err() {
let entry_path = entry.path();
if predicate(entry_path.as_path())
&& let Ok(metadata) = fs::metadata(&entry_path).await
{
if metadata.is_file() {
fs::remove_file(&entry_path).await.log_err();
} else {
fs::remove_dir_all(&entry_path).await.log_err();
}
}
}
}
}
if let Some(mut entries) = fs::read_dir(dir).await.log_err() {
while let Some(entry) = entries.next().await {
if let Some(entry) = entry.log_err() {
let entry_path = entry.path();
if predicate(entry_path.as_path())
&& let Ok(metadata) = fs::metadata(&entry_path).await
{
if metadata.is_file() {
fs::remove_file(&entry_path).await.log_err();
} else {
fs::remove_dir_all(&entry_path).await.log_err();
}
}
}
}
}
}
pub async fn collect_matching<F>(dir: &Path, predicate: F) -> Vec<PathBuf>
where
F: Fn(&Path) -> bool,
F: Fn(&Path) -> bool,
{
let mut matching = vec![];
let mut matching = vec![];
if let Some(mut entries) = fs::read_dir(dir).await.log_err() {
while let Some(entry) = entries.next().await {
if let Some(entry) = entry.log_err()
&& predicate(entry.path().as_path())
{
matching.push(entry.path());
}
}
}
if let Some(mut entries) = fs::read_dir(dir).await.log_err() {
while let Some(entry) = entries.next().await {
if let Some(entry) = entry.log_err()
&& predicate(entry.path().as_path())
{
matching.push(entry.path());
}
}
}
matching
matching
}
pub async fn find_file_name_in_dir<F>(dir: &Path, predicate: F) -> Option<PathBuf>
where
F: Fn(&str) -> bool,
F: Fn(&str) -> bool,
{
if let Some(mut entries) = fs::read_dir(dir).await.log_err() {
while let Some(entry) = entries.next().await {
if let Some(entry) = entry.log_err() {
let entry_path = entry.path();
if let Some(mut entries) = fs::read_dir(dir).await.log_err() {
while let Some(entry) = entries.next().await {
if let Some(entry) = entry.log_err() {
let entry_path = entry.path();
if let Some(file_name) = entry_path
.file_name()
.map(|file_name| file_name.to_string_lossy())
&& predicate(&file_name)
{
return Some(entry_path);
}
}
}
}
if let Some(file_name) = entry_path
.file_name()
.map(|file_name| file_name.to_string_lossy())
&& predicate(&file_name)
{
return Some(entry_path);
}
}
}
}
None
None
}
pub async fn move_folder_files_to_folder<P: AsRef<Path>>(
source_path: P,
target_path: P,
source_path: P,
target_path: P,
) -> Result<()> {
if !target_path.as_ref().is_dir() {
bail!("Folder not found or is not a directory");
}
if !target_path.as_ref().is_dir() {
bail!("Folder not found or is not a directory");
}
let mut entries = fs::read_dir(source_path.as_ref()).await?;
while let Some(entry) = entries.next().await {
let entry = entry?;
let old_path = entry.path();
let new_path = target_path.as_ref().join(entry.file_name());
let mut entries = fs::read_dir(source_path.as_ref()).await?;
while let Some(entry) = entries.next().await {
let entry = entry?;
let old_path = entry.path();
let new_path = target_path.as_ref().join(entry.file_name());
fs::rename(&old_path, &new_path).await?;
}
fs::rename(&old_path, &new_path).await?;
}
fs::remove_dir(source_path).await?;
fs::remove_dir(source_path).await?;
Ok(())
Ok(())
}
#[cfg(unix)]
/// Set the permissions for the given path so that the file becomes executable.
/// This is a noop for non-unix platforms.
pub async fn make_file_executable(path: &Path) -> std::io::Result<()> {
fs::set_permissions(
path,
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
)
.await
fs::set_permissions(
path,
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
)
.await
}
#[cfg(not(unix))]
@@ -107,5 +107,5 @@ pub async fn make_file_executable(path: &Path) -> std::io::Result<()> {
/// Set the permissions for the given path so that the file becomes executable.
/// This is a noop for non-unix platforms.
pub async fn make_file_executable(_path: &Path) -> std::io::Result<()> {
Ok(())
Ok(())
}
+200 -200
View File
@@ -2,59 +2,59 @@ use std::fmt::{Display, Formatter};
/// Generates a URL-friendly slug from heading text (e.g. "Hello World" → "hello-world").
pub fn generate_heading_slug(text: &str) -> String {
text.trim()
.chars()
.filter_map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' {
Some(c.to_lowercase().next().unwrap_or(c))
} else if c == ' ' {
Some('-')
} else {
None
}
})
.collect()
text.trim()
.chars()
.filter_map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' {
Some(c.to_lowercase().next().unwrap_or(c))
} else if c == ' ' {
Some('-')
} else {
None
}
})
.collect()
}
/// Returns true if the URL starts with a URI scheme (RFC 3986 §3.1).
fn has_uri_scheme(url: &str) -> bool {
let mut chars = url.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() => {}
_ => return false,
}
for c in chars {
if c == ':' {
return true;
}
if !(c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.') {
return false;
}
}
false
let mut chars = url.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() => {}
_ => return false,
}
for c in chars {
if c == ':' {
return true;
}
if !(c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.') {
return false;
}
}
false
}
/// Splits a relative URL into its path and `#fragment` parts.
/// Absolute URLs are returned as-is with no fragment.
pub fn split_local_url_fragment(url: &str) -> (&str, Option<&str>) {
if has_uri_scheme(url) {
return (url, None);
}
match url.find('#') {
Some(pos) => {
let path = &url[..pos];
let fragment = &url[pos + 1..];
(
path,
if fragment.is_empty() {
None
} else {
Some(fragment)
},
)
}
None => (url, None),
}
if has_uri_scheme(url) {
return (url, None);
}
match url.find('#') {
Some(pos) => {
let path = &url[..pos];
let fragment = &url[pos + 1..];
(
path,
if fragment.is_empty() {
None
} else {
Some(fragment)
},
)
}
None => (url, None),
}
}
/// Indicates that the wrapped `String` is markdown text.
@@ -62,9 +62,9 @@ pub fn split_local_url_fragment(url: &str) -> (&str, Option<&str>) {
pub struct MarkdownString(pub String);
impl Display for MarkdownString {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// Escapes markdown special characters in markdown text blocks. Markdown code blocks follow
@@ -98,15 +98,15 @@ pub struct MarkdownInlineCode<'a>(pub &'a str);
/// Implements `Display` to format markdown code blocks, wrapped in 3 or more backticks as needed.
pub struct MarkdownCodeBlock<'a> {
pub tag: &'a str,
pub text: &'a str,
pub tag: &'a str,
pub text: &'a str,
}
impl Display for MarkdownEscaped<'_> {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
let mut start_of_unescaped = None;
for (ix, c) in self.0.char_indices() {
match c {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
let mut start_of_unescaped = None;
for (ix, c) in self.0.char_indices() {
match c {
// Always escaped.
'\\' | '`' | '*' | '_' | '[' | '^' | '$' | '~' | '&' |
// TODO: these only need to be escaped when they are the first non-whitespace
@@ -154,94 +154,94 @@ impl Display for MarkdownEscaped<'_> {
}
}
}
}
if let Some(start_of_unescaped) = start_of_unescaped {
write!(formatter, "{}", &self.0[start_of_unescaped..])?;
}
Ok(())
}
}
if let Some(start_of_unescaped) = start_of_unescaped {
write!(formatter, "{}", &self.0[start_of_unescaped..])?;
}
Ok(())
}
}
impl Display for MarkdownInlineCode<'_> {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
// Apache License 2.0, same as this crate.
//
// Copied from `pulldown-cmark-to-cmark-20.0.0` with modifications:
//
// * Handling of all whitespace. pulldown-cmark-to-cmark is anticipating
// `Code` events parsed by pulldown-cmark.
//
// https://github.com/Byron/pulldown-cmark-to-cmark/blob/3c850de2d3d1d79f19ca5f375e1089a653cf3ff7/src/lib.rs#L290
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
// Apache License 2.0, same as this crate.
//
// Copied from `pulldown-cmark-to-cmark-20.0.0` with modifications:
//
// * Handling of all whitespace. pulldown-cmark-to-cmark is anticipating
// `Code` events parsed by pulldown-cmark.
//
// https://github.com/Byron/pulldown-cmark-to-cmark/blob/3c850de2d3d1d79f19ca5f375e1089a653cf3ff7/src/lib.rs#L290
let mut all_whitespace = true;
let text = self
.0
.chars()
.map(|c| {
if c.is_whitespace() {
' '
} else {
all_whitespace = false;
c
}
})
.collect::<String>();
let mut all_whitespace = true;
let text = self
.0
.chars()
.map(|c| {
if c.is_whitespace() {
' '
} else {
all_whitespace = false;
c
}
})
.collect::<String>();
// When inline code has leading and trailing ' ' characters, additional space is needed
// to escape it, unless all characters are space.
if all_whitespace {
write!(formatter, "`{text}`")
} else {
// More backticks are needed to delimit the inline code than the maximum number of
// backticks in a consecutive run.
let backticks = "`".repeat(count_max_consecutive_chars(&text, '`') + 1);
let space = match text.as_bytes() {
&[b'`', ..] | &[.., b'`'] => " ", // Space needed to separate backtick.
&[b' ', .., b' '] => " ", // Space needed to escape inner space.
_ => "", // No space needed.
};
write!(formatter, "{backticks}{space}{text}{space}{backticks}")
}
}
// When inline code has leading and trailing ' ' characters, additional space is needed
// to escape it, unless all characters are space.
if all_whitespace {
write!(formatter, "`{text}`")
} else {
// More backticks are needed to delimit the inline code than the maximum number of
// backticks in a consecutive run.
let backticks = "`".repeat(count_max_consecutive_chars(&text, '`') + 1);
let space = match text.as_bytes() {
&[b'`', ..] | &[.., b'`'] => " ", // Space needed to separate backtick.
&[b' ', .., b' '] => " ", // Space needed to escape inner space.
_ => "", // No space needed.
};
write!(formatter, "{backticks}{space}{text}{space}{backticks}")
}
}
}
impl Display for MarkdownCodeBlock<'_> {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
let tag = self.tag;
let text = self.text;
let backticks = "`".repeat(3.max(count_max_consecutive_chars(text, '`') + 1));
write!(formatter, "{backticks}{tag}\n{text}\n{backticks}\n")
}
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
let tag = self.tag;
let text = self.text;
let backticks = "`".repeat(3.max(count_max_consecutive_chars(text, '`') + 1));
write!(formatter, "{backticks}{tag}\n{text}\n{backticks}\n")
}
}
// Copied from `pulldown-cmark-to-cmark-20.0.0` with changed names.
// https://github.com/Byron/pulldown-cmark-to-cmark/blob/3c850de2d3d1d79f19ca5f375e1089a653cf3ff7/src/lib.rs#L1063
// Apache License 2.0, same as this code.
fn count_max_consecutive_chars(text: &str, search: char) -> usize {
let mut in_search_chars = false;
let mut max_count = 0;
let mut cur_count = 0;
let mut in_search_chars = false;
let mut max_count = 0;
let mut cur_count = 0;
for ch in text.chars() {
if ch == search {
cur_count += 1;
in_search_chars = true;
} else if in_search_chars {
max_count = max_count.max(cur_count);
cur_count = 0;
in_search_chars = false;
}
}
max_count.max(cur_count)
for ch in text.chars() {
if ch == search {
cur_count += 1;
in_search_chars = true;
} else if in_search_chars {
max_count = max_count.max(cur_count);
cur_count = 0;
in_search_chars = false;
}
}
max_count.max(cur_count)
}
#[cfg(test)]
mod tests {
use super::*;
use super::*;
#[test]
fn test_markdown_escaped() {
let input = r#"
#[test]
fn test_markdown_escaped() {
let input = r#"
# Heading
Another heading
@@ -266,7 +266,7 @@ mod tests {
HTML entity: &nbsp;
"#;
let expected = r#"
let expected = r#"
\# Heading
Another heading
@@ -291,86 +291,86 @@ mod tests {
HTML entity: \&nbsp;
"#;
assert_eq!(MarkdownEscaped(input).to_string(), expected);
}
assert_eq!(MarkdownEscaped(input).to_string(), expected);
}
#[test]
fn test_markdown_inline_code() {
assert_eq!(MarkdownInlineCode(" ").to_string(), "` `");
assert_eq!(MarkdownInlineCode("text").to_string(), "`text`");
assert_eq!(MarkdownInlineCode("text ").to_string(), "`text `");
assert_eq!(MarkdownInlineCode(" text ").to_string(), "` text `");
assert_eq!(MarkdownInlineCode("`").to_string(), "`` ` ``");
assert_eq!(MarkdownInlineCode("``").to_string(), "``` `` ```");
assert_eq!(MarkdownInlineCode("`text`").to_string(), "`` `text` ``");
assert_eq!(
MarkdownInlineCode("some `text` no leading or trailing backticks").to_string(),
"``some `text` no leading or trailing backticks``"
);
}
#[test]
fn test_markdown_inline_code() {
assert_eq!(MarkdownInlineCode(" ").to_string(), "` `");
assert_eq!(MarkdownInlineCode("text").to_string(), "`text`");
assert_eq!(MarkdownInlineCode("text ").to_string(), "`text `");
assert_eq!(MarkdownInlineCode(" text ").to_string(), "` text `");
assert_eq!(MarkdownInlineCode("`").to_string(), "`` ` ``");
assert_eq!(MarkdownInlineCode("``").to_string(), "``` `` ```");
assert_eq!(MarkdownInlineCode("`text`").to_string(), "`` `text` ``");
assert_eq!(
MarkdownInlineCode("some `text` no leading or trailing backticks").to_string(),
"``some `text` no leading or trailing backticks``"
);
}
#[test]
fn test_count_max_consecutive_chars() {
assert_eq!(
count_max_consecutive_chars("``a```b``", '`'),
3,
"the highest seen consecutive segment of backticks counts"
);
assert_eq!(
count_max_consecutive_chars("```a``b`", '`'),
3,
"it can't be downgraded later"
);
}
#[test]
fn test_count_max_consecutive_chars() {
assert_eq!(
count_max_consecutive_chars("``a```b``", '`'),
3,
"the highest seen consecutive segment of backticks counts"
);
assert_eq!(
count_max_consecutive_chars("```a``b`", '`'),
3,
"it can't be downgraded later"
);
}
#[test]
fn test_split_local_url_fragment() {
assert_eq!(split_local_url_fragment("#heading"), ("", Some("heading")));
assert_eq!(
split_local_url_fragment("./file.md#heading"),
("./file.md", Some("heading"))
);
assert_eq!(split_local_url_fragment("./file.md"), ("./file.md", None));
assert_eq!(
split_local_url_fragment("https://example.com#frag"),
("https://example.com#frag", None)
);
assert_eq!(
split_local_url_fragment("mailto:user@example.com"),
("mailto:user@example.com", None)
);
assert_eq!(split_local_url_fragment("#"), ("", None));
assert_eq!(
split_local_url_fragment("../other.md#section"),
("../other.md", Some("section"))
);
assert_eq!(
split_local_url_fragment("123:not-a-scheme#frag"),
("123:not-a-scheme", Some("frag"))
);
}
#[test]
fn test_split_local_url_fragment() {
assert_eq!(split_local_url_fragment("#heading"), ("", Some("heading")));
assert_eq!(
split_local_url_fragment("./file.md#heading"),
("./file.md", Some("heading"))
);
assert_eq!(split_local_url_fragment("./file.md"), ("./file.md", None));
assert_eq!(
split_local_url_fragment("https://example.com#frag"),
("https://example.com#frag", None)
);
assert_eq!(
split_local_url_fragment("mailto:user@example.com"),
("mailto:user@example.com", None)
);
assert_eq!(split_local_url_fragment("#"), ("", None));
assert_eq!(
split_local_url_fragment("../other.md#section"),
("../other.md", Some("section"))
);
assert_eq!(
split_local_url_fragment("123:not-a-scheme#frag"),
("123:not-a-scheme", Some("frag"))
);
}
#[test]
fn test_generate_heading_slug() {
assert_eq!(generate_heading_slug("Hello World"), "hello-world");
assert_eq!(generate_heading_slug("Hello World"), "hello--world");
assert_eq!(generate_heading_slug("Hello-World"), "hello-world");
assert_eq!(
generate_heading_slug("Some **bold** text"),
"some-bold-text"
);
assert_eq!(generate_heading_slug("Let's try with Ü"), "lets-try-with-ü");
assert_eq!(
generate_heading_slug("heading with 123 numbers"),
"heading-with-123-numbers"
);
assert_eq!(
generate_heading_slug("What about (parens)?"),
"what-about-parens"
);
assert_eq!(
generate_heading_slug(" leading spaces "),
"leading-spaces"
);
}
#[test]
fn test_generate_heading_slug() {
assert_eq!(generate_heading_slug("Hello World"), "hello-world");
assert_eq!(generate_heading_slug("Hello World"), "hello--world");
assert_eq!(generate_heading_slug("Hello-World"), "hello-world");
assert_eq!(
generate_heading_slug("Some **bold** text"),
"some-bold-text"
);
assert_eq!(generate_heading_slug("Let's try with Ü"), "lets-try-with-ü");
assert_eq!(
generate_heading_slug("heading with 123 numbers"),
"heading-with-123-numbers"
);
assert_eq!(
generate_heading_slug("What about (parens)?"),
"what-about-parens"
);
assert_eq!(
generate_heading_slug(" leading spaces "),
"leading-spaces"
);
}
}
+172 -172
View File
@@ -1,7 +1,7 @@
use std::{
hash::{Hash, Hasher},
path::{Path, PathBuf},
sync::Arc,
hash::{Hash, Hasher},
path::{Path, PathBuf},
sync::Arc,
};
use crate::paths::SanitizedPath;
@@ -16,218 +16,218 @@ use serde::{Deserialize, Serialize};
/// The paths can be retrieved in the original order using `ordered_paths()`.
#[derive(Default, Debug, Clone)]
pub struct PathList {
/// The paths, in lexicographic order.
paths: Arc<[PathBuf]>,
/// The order in which the paths were provided.
///
/// See `ordered_paths()` for a way to get the paths in the original order.
order: Arc<[usize]>,
/// The paths, in lexicographic order.
paths: Arc<[PathBuf]>,
/// The order in which the paths were provided.
///
/// See `ordered_paths()` for a way to get the paths in the original order.
order: Arc<[usize]>,
}
impl PartialEq for PathList {
fn eq(&self, other: &Self) -> bool {
self.paths == other.paths
}
fn eq(&self, other: &Self) -> bool {
self.paths == other.paths
}
}
impl Eq for PathList {}
impl Hash for PathList {
fn hash<H: Hasher>(&self, state: &mut H) {
self.paths.hash(state);
}
fn hash<H: Hasher>(&self, state: &mut H) {
self.paths.hash(state);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializedPathList {
pub paths: String,
pub order: String,
pub paths: String,
pub order: String,
}
impl PathList {
pub fn new<P: AsRef<Path>>(paths: &[P]) -> Self {
let mut indexed_paths: Vec<(usize, PathBuf)> = paths
.iter()
.enumerate()
.map(|(ix, path)| (ix, SanitizedPath::new(path).into()))
.collect();
indexed_paths.sort_by(|(_, a), (_, b)| a.cmp(b));
let order = indexed_paths.iter().map(|e| e.0).collect::<Vec<_>>().into();
let paths = indexed_paths
.into_iter()
.map(|e| e.1)
.collect::<Vec<_>>()
.into();
Self { order, paths }
}
pub fn new<P: AsRef<Path>>(paths: &[P]) -> Self {
let mut indexed_paths: Vec<(usize, PathBuf)> = paths
.iter()
.enumerate()
.map(|(ix, path)| (ix, SanitizedPath::new(path).into()))
.collect();
indexed_paths.sort_by(|(_, a), (_, b)| a.cmp(b));
let order = indexed_paths.iter().map(|e| e.0).collect::<Vec<_>>().into();
let paths = indexed_paths
.into_iter()
.map(|e| e.1)
.collect::<Vec<_>>()
.into();
Self { order, paths }
}
pub fn is_empty(&self) -> bool {
self.paths.is_empty()
}
pub fn is_empty(&self) -> bool {
self.paths.is_empty()
}
/// Returns a new `PathList` with the given path removed.
pub fn without_path(&self, path_to_remove: &Path) -> PathList {
let paths: Vec<PathBuf> = self
.ordered_paths()
.filter(|p| p.as_path() != path_to_remove)
.cloned()
.collect();
PathList::new(&paths)
}
/// Returns a new `PathList` with the given path removed.
pub fn without_path(&self, path_to_remove: &Path) -> PathList {
let paths: Vec<PathBuf> = self
.ordered_paths()
.filter(|p| p.as_path() != path_to_remove)
.cloned()
.collect();
PathList::new(&paths)
}
/// Get the paths in lexicographic order.
pub fn paths(&self) -> &[PathBuf] {
self.paths.as_ref()
}
/// Get the paths in lexicographic order.
pub fn paths(&self) -> &[PathBuf] {
self.paths.as_ref()
}
/// Get the paths in the lexicographic order.
pub fn paths_owned(&self) -> Arc<[PathBuf]> {
self.paths.clone()
}
/// Get the paths in the lexicographic order.
pub fn paths_owned(&self) -> Arc<[PathBuf]> {
self.paths.clone()
}
/// Get the order in which the paths were provided.
pub fn order(&self) -> &[usize] {
self.order.as_ref()
}
/// Get the order in which the paths were provided.
pub fn order(&self) -> &[usize] {
self.order.as_ref()
}
/// Get the paths in the original order.
pub fn ordered_paths(&self) -> impl Iterator<Item = &PathBuf> {
self.order
.iter()
.zip(self.paths.iter())
.sorted_by_key(|(i, _)| **i)
.map(|(_, path)| path)
}
/// Get the paths in the original order.
pub fn ordered_paths(&self) -> impl Iterator<Item = &PathBuf> {
self.order
.iter()
.zip(self.paths.iter())
.sorted_by_key(|(i, _)| **i)
.map(|(_, path)| path)
}
pub fn is_lexicographically_ordered(&self) -> bool {
self.order.iter().enumerate().all(|(i, &j)| i == j)
}
pub fn is_lexicographically_ordered(&self) -> bool {
self.order.iter().enumerate().all(|(i, &j)| i == j)
}
pub fn deserialize(serialized: &SerializedPathList) -> Self {
let mut paths: Vec<PathBuf> = if serialized.paths.is_empty() {
Vec::new()
} else {
serialized.paths.split('\n').map(PathBuf::from).collect()
};
pub fn deserialize(serialized: &SerializedPathList) -> Self {
let mut paths: Vec<PathBuf> = if serialized.paths.is_empty() {
Vec::new()
} else {
serialized.paths.split('\n').map(PathBuf::from).collect()
};
let mut order: Vec<usize> = serialized
.order
.split(',')
.filter_map(|s| s.parse().ok())
.collect();
let mut order: Vec<usize> = serialized
.order
.split(',')
.filter_map(|s| s.parse().ok())
.collect();
if !paths.is_sorted() || order.len() != paths.len() {
order = (0..paths.len()).collect();
paths.sort();
}
if !paths.is_sorted() || order.len() != paths.len() {
order = (0..paths.len()).collect();
paths.sort();
}
Self {
paths: paths.into(),
order: order.into(),
}
}
Self {
paths: paths.into(),
order: order.into(),
}
}
pub fn serialize(&self) -> SerializedPathList {
use std::fmt::Write as _;
pub fn serialize(&self) -> SerializedPathList {
use std::fmt::Write as _;
let mut paths = String::new();
for path in self.paths.iter() {
if !paths.is_empty() {
paths.push('\n');
}
paths.push_str(&path.to_string_lossy());
}
let mut paths = String::new();
for path in self.paths.iter() {
if !paths.is_empty() {
paths.push('\n');
}
paths.push_str(&path.to_string_lossy());
}
let mut order = String::new();
for ix in self.order.iter() {
if !order.is_empty() {
order.push(',');
}
write!(&mut order, "{}", *ix).unwrap();
}
SerializedPathList { paths, order }
}
let mut order = String::new();
for ix in self.order.iter() {
if !order.is_empty() {
order.push(',');
}
write!(&mut order, "{}", *ix).unwrap();
}
SerializedPathList { paths, order }
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::*;
#[test]
fn test_path_list() {
let list1 = PathList::new(&["a/d", "a/c"]);
let list2 = PathList::new(&["a/c", "a/d"]);
#[test]
fn test_path_list() {
let list1 = PathList::new(&["a/d", "a/c"]);
let list2 = PathList::new(&["a/c", "a/d"]);
assert_eq!(list1.paths(), list2.paths(), "paths differ");
assert_eq!(list1.order(), &[1, 0], "list1 order incorrect");
assert_eq!(list2.order(), &[0, 1], "list2 order incorrect");
assert_eq!(list1.paths(), list2.paths(), "paths differ");
assert_eq!(list1.order(), &[1, 0], "list1 order incorrect");
assert_eq!(list2.order(), &[0, 1], "list2 order incorrect");
// Same paths in different order are equal (order is display-only).
assert_eq!(
list1, list2,
"same paths with different order should be equal"
);
// Same paths in different order are equal (order is display-only).
assert_eq!(
list1, list2,
"same paths with different order should be equal"
);
let list1_deserialized = PathList::deserialize(&list1.serialize());
assert_eq!(list1_deserialized, list1, "list1 deserialization failed");
let list1_deserialized = PathList::deserialize(&list1.serialize());
assert_eq!(list1_deserialized, list1, "list1 deserialization failed");
let list2_deserialized = PathList::deserialize(&list2.serialize());
assert_eq!(list2_deserialized, list2, "list2 deserialization failed");
let list2_deserialized = PathList::deserialize(&list2.serialize());
assert_eq!(list2_deserialized, list2, "list2 deserialization failed");
assert_eq!(
list1.ordered_paths().collect_array().unwrap(),
[&PathBuf::from("a/d"), &PathBuf::from("a/c")],
"list1 ordered paths incorrect"
);
assert_eq!(
list2.ordered_paths().collect_array().unwrap(),
[&PathBuf::from("a/c"), &PathBuf::from("a/d")],
"list2 ordered paths incorrect"
);
}
assert_eq!(
list1.ordered_paths().collect_array().unwrap(),
[&PathBuf::from("a/d"), &PathBuf::from("a/c")],
"list1 ordered paths incorrect"
);
assert_eq!(
list2.ordered_paths().collect_array().unwrap(),
[&PathBuf::from("a/c"), &PathBuf::from("a/d")],
"list2 ordered paths incorrect"
);
}
#[test]
fn test_path_list_ordering() {
let list = PathList::new(&["b", "a", "c"]);
assert_eq!(
list.paths(),
&[PathBuf::from("a"), PathBuf::from("b"), PathBuf::from("c")]
);
assert_eq!(list.order(), &[1, 0, 2]);
assert!(!list.is_lexicographically_ordered());
#[test]
fn test_path_list_ordering() {
let list = PathList::new(&["b", "a", "c"]);
assert_eq!(
list.paths(),
&[PathBuf::from("a"), PathBuf::from("b"), PathBuf::from("c")]
);
assert_eq!(list.order(), &[1, 0, 2]);
assert!(!list.is_lexicographically_ordered());
let serialized = list.serialize();
let deserialized = PathList::deserialize(&serialized);
assert_eq!(deserialized, list);
let serialized = list.serialize();
let deserialized = PathList::deserialize(&serialized);
assert_eq!(deserialized, list);
assert_eq!(
deserialized.ordered_paths().collect_array().unwrap(),
[
&PathBuf::from("b"),
&PathBuf::from("a"),
&PathBuf::from("c")
]
);
assert_eq!(
deserialized.ordered_paths().collect_array().unwrap(),
[
&PathBuf::from("b"),
&PathBuf::from("a"),
&PathBuf::from("c")
]
);
let list = PathList::new(&["b", "c", "a"]);
assert_eq!(
list.paths(),
&[PathBuf::from("a"), PathBuf::from("b"), PathBuf::from("c")]
);
assert_eq!(list.order(), &[2, 0, 1]);
assert!(!list.is_lexicographically_ordered());
let list = PathList::new(&["b", "c", "a"]);
assert_eq!(
list.paths(),
&[PathBuf::from("a"), PathBuf::from("b"), PathBuf::from("c")]
);
assert_eq!(list.order(), &[2, 0, 1]);
assert!(!list.is_lexicographically_ordered());
let serialized = list.serialize();
let deserialized = PathList::deserialize(&serialized);
assert_eq!(deserialized, list);
let serialized = list.serialize();
let deserialized = PathList::deserialize(&serialized);
assert_eq!(deserialized, list);
assert_eq!(
deserialized.ordered_paths().collect_array().unwrap(),
[
&PathBuf::from("b"),
&PathBuf::from("c"),
&PathBuf::from("a"),
]
);
}
assert_eq!(
deserialized.ordered_paths().collect_array().unwrap(),
[
&PathBuf::from("b"),
&PathBuf::from("c"),
&PathBuf::from("a"),
]
);
}
}
File diff suppressed because it is too large Load Diff
+70 -70
View File
@@ -4,89 +4,89 @@ use std::process::Stdio;
/// A wrapper around `smol::process::Child` that ensures all subprocesses
/// are killed when the process is terminated by using process groups.
pub struct Child {
process: smol::process::Child,
process: smol::process::Child,
}
impl std::ops::Deref for Child {
type Target = smol::process::Child;
type Target = smol::process::Child;
fn deref(&self) -> &Self::Target {
&self.process
}
fn deref(&self) -> &Self::Target {
&self.process
}
}
impl std::ops::DerefMut for Child {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.process
}
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.process
}
}
impl Child {
#[cfg(not(windows))]
pub fn spawn(
mut command: std::process::Command,
stdin: Stdio,
stdout: Stdio,
stderr: Stdio,
) -> Result<Self> {
crate::set_pre_exec_to_start_new_session(&mut command);
let mut command = smol::process::Command::from(command);
let process = command
.stdin(stdin)
.stdout(stdout)
.stderr(stderr)
.spawn()
.with_context(|| {
format!(
"failed to spawn command {}",
crate::redact::redact_command(&format!("{command:?}"))
)
})?;
Ok(Self { process })
}
#[cfg(not(windows))]
pub fn spawn(
mut command: std::process::Command,
stdin: Stdio,
stdout: Stdio,
stderr: Stdio,
) -> Result<Self> {
crate::set_pre_exec_to_start_new_session(&mut command);
let mut command = smol::process::Command::from(command);
let process = command
.stdin(stdin)
.stdout(stdout)
.stderr(stderr)
.spawn()
.with_context(|| {
format!(
"failed to spawn command {}",
crate::redact::redact_command(&format!("{command:?}"))
)
})?;
Ok(Self { process })
}
#[cfg(windows)]
pub fn spawn(
command: std::process::Command,
stdin: Stdio,
stdout: Stdio,
stderr: Stdio,
) -> Result<Self> {
// TODO(windows): create a job object and add the child process handle to it,
// see https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects
let mut command = smol::process::Command::from(command);
let process = command
.stdin(stdin)
.stdout(stdout)
.stderr(stderr)
.spawn()
.with_context(|| {
format!(
"failed to spawn command {}",
crate::redact::redact_command(&format!("{command:?}"))
)
})?;
#[cfg(windows)]
pub fn spawn(
command: std::process::Command,
stdin: Stdio,
stdout: Stdio,
stderr: Stdio,
) -> Result<Self> {
// TODO(windows): create a job object and add the child process handle to it,
// see https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects
let mut command = smol::process::Command::from(command);
let process = command
.stdin(stdin)
.stdout(stdout)
.stderr(stderr)
.spawn()
.with_context(|| {
format!(
"failed to spawn command {}",
crate::redact::redact_command(&format!("{command:?}"))
)
})?;
Ok(Self { process })
}
Ok(Self { process })
}
pub fn into_inner(self) -> smol::process::Child {
self.process
}
pub fn into_inner(self) -> smol::process::Child {
self.process
}
#[cfg(not(windows))]
pub fn kill(&mut self) -> Result<()> {
let pid = self.process.id();
unsafe {
libc::killpg(pid as i32, libc::SIGKILL);
}
Ok(())
}
#[cfg(not(windows))]
pub fn kill(&mut self) -> Result<()> {
let pid = self.process.id();
unsafe {
libc::killpg(pid as i32, libc::SIGKILL);
}
Ok(())
}
#[cfg(windows)]
pub fn kill(&mut self) -> Result<()> {
// TODO(windows): terminate the job object in kill
self.process.kill()?;
Ok(())
}
#[cfg(windows)]
pub fn kill(&mut self) -> Result<()> {
// TODO(windows): terminate the job object in kill
self.process.kill()?;
Ok(())
}
}
+32 -32
View File
@@ -1,49 +1,49 @@
use std::sync::LazyLock;
static REDACT_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r#"([A-Z_][A-Z0-9_]*)=("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\S+)"#).unwrap()
regex::Regex::new(r#"([A-Z_][A-Z0-9_]*)=("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\S+)"#).unwrap()
});
/// Whether a given environment variable name should have its value redacted
pub fn should_redact(env_var_name: &str) -> bool {
const REDACTED_SUFFIXES: &[&str] = &[
"KEY",
"TOKEN",
"PASSWORD",
"SECRET",
"PASS",
"CREDENTIALS",
"LICENSE",
];
REDACTED_SUFFIXES
.iter()
.any(|suffix| env_var_name.ends_with(suffix))
const REDACTED_SUFFIXES: &[&str] = &[
"KEY",
"TOKEN",
"PASSWORD",
"SECRET",
"PASS",
"CREDENTIALS",
"LICENSE",
];
REDACTED_SUFFIXES
.iter()
.any(|suffix| env_var_name.ends_with(suffix))
}
/// Redact a string which could include a command with environment variables
pub fn redact_command(command: &str) -> String {
REDACT_REGEX
.replace_all(command, |caps: &regex::Captures| {
let var_name = &caps[1];
let value = &caps[2];
if should_redact(var_name) {
format!(r#"{}="[REDACTED]""#, var_name)
} else {
format!("{}={}", var_name, value)
}
})
.to_string()
REDACT_REGEX
.replace_all(command, |caps: &regex::Captures| {
let var_name = &caps[1];
let value = &caps[2];
if should_redact(var_name) {
format!(r#"{}="[REDACTED]""#, var_name)
} else {
format!("{}={}", var_name, value)
}
})
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use super::*;
#[test]
fn test_redact_string_with_multiple_env_vars() {
let input = r#"failed to spawn command cd "/code/something" && ANTHROPIC_API_KEY="sk-ant-api03-WOOOO" COMMAND_MODE="unix2003" GEMINI_API_KEY="AIGEMINIFACE" HOME="/Users/foo""#;
let result = redact_command(input);
let expected = r#"failed to spawn command cd "/code/something" && ANTHROPIC_API_KEY="[REDACTED]" COMMAND_MODE="unix2003" GEMINI_API_KEY="[REDACTED]" HOME="/Users/foo""#;
assert_eq!(result, expected);
}
#[test]
fn test_redact_string_with_multiple_env_vars() {
let input = r#"failed to spawn command cd "/code/something" && ANTHROPIC_API_KEY="sk-ant-api03-WOOOO" COMMAND_MODE="unix2003" GEMINI_API_KEY="AIGEMINIFACE" HOME="/Users/foo""#;
let result = redact_command(input);
let expected = r#"failed to spawn command cd "/code/something" && ANTHROPIC_API_KEY="[REDACTED]" COMMAND_MODE="unix2003" GEMINI_API_KEY="[REDACTED]" HOME="/Users/foo""#;
assert_eq!(result, expected);
}
}
File diff suppressed because it is too large Load Diff
+31 -31
View File
@@ -12,27 +12,27 @@ const DEFS_PATH: &str = "#/$defs/";
/// This takes a closure for `schema` because some settings types are not available on the remote
/// server, and so will crash when attempting to access e.g. GlobalThemeRegistry.
pub fn replace_subschema<T: JsonSchema>(
generator: &mut schemars::SchemaGenerator,
schema: impl Fn() -> schemars::Schema,
generator: &mut schemars::SchemaGenerator,
schema: impl Fn() -> schemars::Schema,
) -> schemars::Schema {
let schema_name = T::schema_name();
let definitions = generator.definitions_mut();
assert!(!definitions.contains_key(&format!("{schema_name}2")));
assert!(definitions.contains_key(schema_name.as_ref()));
definitions.insert(schema_name.to_string(), schema().to_value());
schemars::Schema::new_ref(format!("{DEFS_PATH}{schema_name}"))
let schema_name = T::schema_name();
let definitions = generator.definitions_mut();
assert!(!definitions.contains_key(&format!("{schema_name}2")));
assert!(definitions.contains_key(schema_name.as_ref()));
definitions.insert(schema_name.to_string(), schema().to_value());
schemars::Schema::new_ref(format!("{DEFS_PATH}{schema_name}"))
}
/// Adds a new JSON schema definition and returns a reference to it. **Panics** if the name is
/// already in use.
pub fn add_new_subschema(
generator: &mut schemars::SchemaGenerator,
name: &str,
schema: serde_json::Value,
generator: &mut schemars::SchemaGenerator,
name: &str,
schema: serde_json::Value,
) -> schemars::Schema {
let old_definition = generator.definitions_mut().insert(name.to_string(), schema);
assert_eq!(old_definition, None);
schemars::Schema::new_ref(format!("{DEFS_PATH}{name}"))
let old_definition = generator.definitions_mut().insert(name.to_string(), schema);
assert_eq!(old_definition, None);
schemars::Schema::new_ref(format!("{DEFS_PATH}{name}"))
}
/// Defaults `additionalProperties` to `true`, as if `#[schemars(deny_unknown_fields)]` was on every
@@ -42,16 +42,16 @@ pub fn add_new_subschema(
pub struct DefaultDenyUnknownFields;
impl schemars::transform::Transform for DefaultDenyUnknownFields {
fn transform(&mut self, schema: &mut schemars::Schema) {
if let Some(object) = schema.as_object_mut()
&& object.contains_key("properties")
&& !object.contains_key("additionalProperties")
&& !object.contains_key("unevaluatedProperties")
{
object.insert("additionalProperties".to_string(), false.into());
}
transform_subschemas(self, schema);
}
fn transform(&mut self, schema: &mut schemars::Schema) {
if let Some(object) = schema.as_object_mut()
&& object.contains_key("properties")
&& !object.contains_key("additionalProperties")
&& !object.contains_key("unevaluatedProperties")
{
object.insert("additionalProperties".to_string(), false.into());
}
transform_subschemas(self, schema);
}
}
/// Defaults `allowTrailingCommas` to `true`, for use with `json-language-server`.
@@ -62,11 +62,11 @@ impl schemars::transform::Transform for DefaultDenyUnknownFields {
pub struct AllowTrailingCommas;
impl schemars::transform::Transform for AllowTrailingCommas {
fn transform(&mut self, schema: &mut schemars::Schema) {
if let Some(object) = schema.as_object_mut()
&& !object.contains_key("allowTrailingCommas")
{
object.insert("allowTrailingCommas".to_string(), true.into());
}
}
fn transform(&mut self, schema: &mut schemars::Schema) {
if let Some(object) = schema.as_object_mut()
&& !object.contains_key("allowTrailingCommas")
{
object.insert("allowTrailingCommas".to_string(), true.into());
}
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
pub const fn default_true() -> bool {
true
true
}
pub fn is_default<T: Default + PartialEq>(value: &T) -> bool {
*value == T::default()
*value == T::default()
}
File diff suppressed because it is too large Load Diff
+276 -276
View File
@@ -6,322 +6,322 @@ use crate::shell::{Shell, ShellKind};
/// ShellBuilder is used to turn a user-requested task into a
/// program that can be executed by the shell.
pub struct ShellBuilder {
/// The shell to run
program: String,
args: Vec<String>,
interactive: bool,
/// Whether to redirect stdin to /dev/null for the spawned command as a subshell.
redirect_stdin: bool,
kind: ShellKind,
/// The shell to run
program: String,
args: Vec<String>,
interactive: bool,
/// Whether to redirect stdin to /dev/null for the spawned command as a subshell.
redirect_stdin: bool,
kind: ShellKind,
}
impl ShellBuilder {
/// Create a new ShellBuilder as configured.
pub fn new(shell: &Shell, is_windows: bool) -> Self {
let (program, args) = match shell {
Shell::System => (get_system_shell(), Vec::new()),
Shell::Program(shell) => (shell.clone(), Vec::new()),
Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()),
};
/// Create a new ShellBuilder as configured.
pub fn new(shell: &Shell, is_windows: bool) -> Self {
let (program, args) = match shell {
Shell::System => (get_system_shell(), Vec::new()),
Shell::Program(shell) => (shell.clone(), Vec::new()),
Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()),
};
let kind = ShellKind::new(&program, is_windows);
Self {
program,
args,
interactive: true,
kind,
redirect_stdin: false,
}
}
pub fn non_interactive(mut self) -> Self {
self.interactive = false;
self
}
let kind = ShellKind::new(&program, is_windows);
Self {
program,
args,
interactive: true,
kind,
redirect_stdin: false,
}
}
pub fn non_interactive(mut self) -> Self {
self.interactive = false;
self
}
/// Returns the label to show in the terminal tab
pub fn command_label(&self, command_to_use_in_label: &str) -> String {
if command_to_use_in_label.trim().is_empty() {
self.program.clone()
} else {
match self.kind {
ShellKind::PowerShell | ShellKind::Pwsh => {
format!("{} -C '{}'", self.program, command_to_use_in_label)
}
ShellKind::Cmd => {
format!("{} /C \"{}\"", self.program, command_to_use_in_label)
}
ShellKind::Posix
| ShellKind::Nushell
| ShellKind::Fish
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
| ShellKind::Xonsh
| ShellKind::Elvish => {
let interactivity = if self.interactive { "-i " } else { "" };
format!(
"{PROGRAM} {interactivity}-c '{command_to_use_in_label}'",
PROGRAM = self.program
)
}
}
}
}
/// Returns the label to show in the terminal tab
pub fn command_label(&self, command_to_use_in_label: &str) -> String {
if command_to_use_in_label.trim().is_empty() {
self.program.clone()
} else {
match self.kind {
ShellKind::PowerShell | ShellKind::Pwsh => {
format!("{} -C '{}'", self.program, command_to_use_in_label)
}
ShellKind::Cmd => {
format!("{} /C \"{}\"", self.program, command_to_use_in_label)
}
ShellKind::Posix
| ShellKind::Nushell
| ShellKind::Fish
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
| ShellKind::Xonsh
| ShellKind::Elvish => {
let interactivity = if self.interactive { "-i " } else { "" };
format!(
"{PROGRAM} {interactivity}-c '{command_to_use_in_label}'",
PROGRAM = self.program
)
}
}
}
}
pub fn redirect_stdin_to_dev_null(mut self) -> Self {
self.redirect_stdin = true;
self
}
pub fn redirect_stdin_to_dev_null(mut self) -> Self {
self.redirect_stdin = true;
self
}
/// Returns the program and arguments to run this task in a shell.
pub fn build(
mut self,
task_command: Option<String>,
task_args: &[String],
) -> (String, Vec<String>) {
if let Some(task_command) = task_command {
let task_command = if !task_args.is_empty() {
match self.kind.try_quote_prefix_aware(&task_command) {
Some(task_command) => task_command.into_owned(),
None => task_command,
}
} else {
task_command
};
let mut combined_command = task_args.iter().fold(task_command, |mut command, arg| {
command.push(' ');
let shell_variable = self.kind.to_shell_variable(arg);
command.push_str(&match self.kind.try_quote(&shell_variable) {
Some(shell_variable) => shell_variable,
None => Cow::Owned(shell_variable),
});
command
});
if self.redirect_stdin {
match self.kind {
ShellKind::Fish => {
combined_command.insert_str(0, "begin; ");
combined_command.push_str("; end </dev/null");
}
ShellKind::Posix
| ShellKind::Nushell
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
| ShellKind::Xonsh
| ShellKind::Elvish => {
combined_command.insert(0, '(');
combined_command.push_str("\n) </dev/null");
}
ShellKind::PowerShell | ShellKind::Pwsh => {
combined_command.insert_str(0, "$null | & {");
combined_command.push('}');
}
ShellKind::Cmd => {
combined_command.push_str("< NUL");
}
}
}
/// Returns the program and arguments to run this task in a shell.
pub fn build(
mut self,
task_command: Option<String>,
task_args: &[String],
) -> (String, Vec<String>) {
if let Some(task_command) = task_command {
let task_command = if !task_args.is_empty() {
match self.kind.try_quote_prefix_aware(&task_command) {
Some(task_command) => task_command.into_owned(),
None => task_command,
}
} else {
task_command
};
let mut combined_command = task_args.iter().fold(task_command, |mut command, arg| {
command.push(' ');
let shell_variable = self.kind.to_shell_variable(arg);
command.push_str(&match self.kind.try_quote(&shell_variable) {
Some(shell_variable) => shell_variable,
None => Cow::Owned(shell_variable),
});
command
});
if self.redirect_stdin {
match self.kind {
ShellKind::Fish => {
combined_command.insert_str(0, "begin; ");
combined_command.push_str("; end </dev/null");
}
ShellKind::Posix
| ShellKind::Nushell
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
| ShellKind::Xonsh
| ShellKind::Elvish => {
combined_command.insert(0, '(');
combined_command.push_str("\n) </dev/null");
}
ShellKind::PowerShell | ShellKind::Pwsh => {
combined_command.insert_str(0, "$null | & {");
combined_command.push('}');
}
ShellKind::Cmd => {
combined_command.push_str("< NUL");
}
}
}
self.args
.extend(self.kind.args_for_shell(self.interactive, combined_command));
}
self.args
.extend(self.kind.args_for_shell(self.interactive, combined_command));
}
(self.program, self.args)
}
(self.program, self.args)
}
// This should not exist, but our task infra is broken beyond repair right now
#[doc(hidden)]
pub fn build_no_quote(
mut self,
task_command: Option<String>,
task_args: &[String],
) -> (String, Vec<String>) {
if let Some(task_command) = task_command {
let mut combined_command = task_args.iter().fold(task_command, |mut command, arg| {
command.push(' ');
command.push_str(&self.kind.to_shell_variable(arg));
command
});
if self.redirect_stdin {
match self.kind {
ShellKind::Fish => {
combined_command.insert_str(0, "begin; ");
combined_command.push_str("; end </dev/null");
}
ShellKind::Posix
| ShellKind::Nushell
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
| ShellKind::Xonsh
| ShellKind::Elvish => {
combined_command.insert(0, '(');
combined_command.push_str("\n) </dev/null");
}
ShellKind::PowerShell | ShellKind::Pwsh => {
combined_command.insert_str(0, "$null | & {");
combined_command.push('}');
}
ShellKind::Cmd => {
combined_command.push_str("< NUL");
}
}
}
// This should not exist, but our task infra is broken beyond repair right now
#[doc(hidden)]
pub fn build_no_quote(
mut self,
task_command: Option<String>,
task_args: &[String],
) -> (String, Vec<String>) {
if let Some(task_command) = task_command {
let mut combined_command = task_args.iter().fold(task_command, |mut command, arg| {
command.push(' ');
command.push_str(&self.kind.to_shell_variable(arg));
command
});
if self.redirect_stdin {
match self.kind {
ShellKind::Fish => {
combined_command.insert_str(0, "begin; ");
combined_command.push_str("; end </dev/null");
}
ShellKind::Posix
| ShellKind::Nushell
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
| ShellKind::Xonsh
| ShellKind::Elvish => {
combined_command.insert(0, '(');
combined_command.push_str("\n) </dev/null");
}
ShellKind::PowerShell | ShellKind::Pwsh => {
combined_command.insert_str(0, "$null | & {");
combined_command.push('}');
}
ShellKind::Cmd => {
combined_command.push_str("< NUL");
}
}
}
self.args
.extend(self.kind.args_for_shell(self.interactive, combined_command));
}
self.args
.extend(self.kind.args_for_shell(self.interactive, combined_command));
}
(self.program, self.args)
}
(self.program, self.args)
}
/// Builds a `smol::process::Command` with the given task command and arguments.
///
/// Prefer this over manually constructing a command with the output of `Self::build`,
/// as this method handles `cmd` weirdness on windows correctly.
pub fn build_smol_command(
self,
task_command: Option<String>,
task_args: &[String],
) -> smol::process::Command {
smol::process::Command::from(self.build_std_command(task_command, task_args))
}
/// Builds a `smol::process::Command` with the given task command and arguments.
///
/// Prefer this over manually constructing a command with the output of `Self::build`,
/// as this method handles `cmd` weirdness on windows correctly.
pub fn build_smol_command(
self,
task_command: Option<String>,
task_args: &[String],
) -> smol::process::Command {
smol::process::Command::from(self.build_std_command(task_command, task_args))
}
/// Builds a `std::process::Command` with the given task command and arguments.
///
/// Prefer this over manually constructing a command with the output of `Self::build`,
/// as this method handles `cmd` weirdness on windows correctly.
pub fn build_std_command(
self,
mut task_command: Option<String>,
task_args: &[String],
) -> std::process::Command {
#[cfg(windows)]
let kind = self.kind;
if task_args.is_empty() {
task_command = task_command
.as_ref()
.map(|cmd| self.kind.try_quote_prefix_aware(cmd).map(Cow::into_owned))
.unwrap_or(task_command);
}
let (program, args) = self.build(task_command, task_args);
/// Builds a `std::process::Command` with the given task command and arguments.
///
/// Prefer this over manually constructing a command with the output of `Self::build`,
/// as this method handles `cmd` weirdness on windows correctly.
pub fn build_std_command(
self,
mut task_command: Option<String>,
task_args: &[String],
) -> std::process::Command {
#[cfg(windows)]
let kind = self.kind;
if task_args.is_empty() {
task_command = task_command
.as_ref()
.map(|cmd| self.kind.try_quote_prefix_aware(cmd).map(Cow::into_owned))
.unwrap_or(task_command);
}
let (program, args) = self.build(task_command, task_args);
let mut child = crate::command::new_std_command(program);
let mut child = crate::command::new_std_command(program);
#[cfg(windows)]
if kind == ShellKind::Cmd {
use std::os::windows::process::CommandExt;
#[cfg(windows)]
if kind == ShellKind::Cmd {
use std::os::windows::process::CommandExt;
for arg in args {
child.raw_arg(arg);
}
} else {
child.args(args);
}
for arg in args {
child.raw_arg(arg);
}
} else {
child.args(args);
}
#[cfg(not(windows))]
child.args(args);
#[cfg(not(windows))]
child.args(args);
child
}
child
}
pub fn kind(&self) -> ShellKind {
self.kind
}
pub fn kind(&self) -> ShellKind {
self.kind
}
}
#[cfg(test)]
mod test {
use super::*;
use super::*;
#[test]
fn test_nu_shell_variable_substitution() {
let shell = Shell::Program("nu".to_owned());
let shell_builder = ShellBuilder::new(&shell, false);
#[test]
fn test_nu_shell_variable_substitution() {
let shell = Shell::Program("nu".to_owned());
let shell_builder = ShellBuilder::new(&shell, false);
let (program, args) = shell_builder.build(
Some("echo".into()),
&[
"${hello}".to_string(),
"$world".to_string(),
"nothing".to_string(),
"--$something".to_string(),
"$".to_string(),
"${test".to_string(),
],
);
let (program, args) = shell_builder.build(
Some("echo".into()),
&[
"${hello}".to_string(),
"$world".to_string(),
"nothing".to_string(),
"--$something".to_string(),
"$".to_string(),
"${test".to_string(),
],
);
assert_eq!(program, "nu");
assert_eq!(
args,
vec![
"-i",
"-c",
"echo '$env.hello' '$env.world' nothing '--($env.something)' '$' '${test'"
]
);
}
assert_eq!(program, "nu");
assert_eq!(
args,
vec![
"-i",
"-c",
"echo '$env.hello' '$env.world' nothing '--($env.something)' '$' '${test'"
]
);
}
#[test]
fn redirect_stdin_to_dev_null_precedence() {
let shell = Shell::Program("nu".to_owned());
let shell_builder = ShellBuilder::new(&shell, false);
#[test]
fn redirect_stdin_to_dev_null_precedence() {
let shell = Shell::Program("nu".to_owned());
let shell_builder = ShellBuilder::new(&shell, false);
let (program, args) = shell_builder
.redirect_stdin_to_dev_null()
.build(Some("echo".into()), &["nothing".to_string()]);
let (program, args) = shell_builder
.redirect_stdin_to_dev_null()
.build(Some("echo".into()), &["nothing".to_string()]);
assert_eq!(program, "nu");
assert_eq!(args, vec!["-i", "-c", "(echo nothing\n) </dev/null"]);
}
assert_eq!(program, "nu");
assert_eq!(args, vec!["-i", "-c", "(echo nothing\n) </dev/null"]);
}
#[test]
fn redirect_stdin_to_dev_null_fish() {
let shell = Shell::Program("fish".to_owned());
let shell_builder = ShellBuilder::new(&shell, false);
#[test]
fn redirect_stdin_to_dev_null_fish() {
let shell = Shell::Program("fish".to_owned());
let shell_builder = ShellBuilder::new(&shell, false);
let (program, args) = shell_builder
.redirect_stdin_to_dev_null()
.build(Some("echo".into()), &["test".to_string()]);
let (program, args) = shell_builder
.redirect_stdin_to_dev_null()
.build(Some("echo".into()), &["test".to_string()]);
assert_eq!(program, "fish");
assert_eq!(args, vec!["-i", "-c", "begin; echo test; end </dev/null"]);
}
assert_eq!(program, "fish");
assert_eq!(args, vec!["-i", "-c", "begin; echo test; end </dev/null"]);
}
#[test]
fn redirect_stdin_to_dev_null_preserves_heredoc() {
let shell = Shell::Program("sh".to_owned());
let shell_builder = ShellBuilder::new(&shell, false);
#[test]
fn redirect_stdin_to_dev_null_preserves_heredoc() {
let shell = Shell::Program("sh".to_owned());
let shell_builder = ShellBuilder::new(&shell, false);
let command = "cat <<EOF\nhello\nEOF";
let (program, args) = shell_builder
.redirect_stdin_to_dev_null()
.build(Some(command.into()), &[]);
let command = "cat <<EOF\nhello\nEOF";
let (program, args) = shell_builder
.redirect_stdin_to_dev_null()
.build(Some(command.into()), &[]);
assert_eq!(program, "sh");
assert_eq!(
args,
vec!["-i", "-c", "(cat <<EOF\nhello\nEOF\n) </dev/null"]
);
}
assert_eq!(program, "sh");
assert_eq!(
args,
vec!["-i", "-c", "(cat <<EOF\nhello\nEOF\n) </dev/null"]
);
}
#[test]
fn does_not_quote_sole_command_only() {
let shell = Shell::Program("fish".to_owned());
let shell_builder = ShellBuilder::new(&shell, false);
#[test]
fn does_not_quote_sole_command_only() {
let shell = Shell::Program("fish".to_owned());
let shell_builder = ShellBuilder::new(&shell, false);
let (program, args) = shell_builder.build(Some("echo".into()), &[]);
let (program, args) = shell_builder.build(Some("echo".into()), &[]);
assert_eq!(program, "fish");
assert_eq!(args, vec!["-i", "-c", "echo"]);
assert_eq!(program, "fish");
assert_eq!(args, vec!["-i", "-c", "echo"]);
let shell = Shell::Program("fish".to_owned());
let shell_builder = ShellBuilder::new(&shell, false);
let shell = Shell::Program("fish".to_owned());
let shell_builder = ShellBuilder::new(&shell, false);
let (program, args) = shell_builder.build(Some("echo oo".into()), &[]);
let (program, args) = shell_builder.build(Some("echo oo".into()), &[]);
assert_eq!(program, "fish");
assert_eq!(args, vec!["-i", "-c", "echo oo"]);
}
assert_eq!(program, "fish");
assert_eq!(args, vec!["-i", "-c", "echo oo"]);
}
}
+269 -269
View File
@@ -7,35 +7,35 @@ use serde::Deserialize;
use crate::shell::ShellKind;
fn parse_env_map_from_noisy_output(output: &str) -> Result<collections::HashMap<String, String>> {
for (position, _) in output.match_indices('{') {
let candidate = &output[position..];
let mut deserializer = serde_json::Deserializer::from_str(candidate);
if let Ok(env_map) = HashMap::<String, String>::deserialize(&mut deserializer) {
return Ok(env_map);
}
}
anyhow::bail!("Failed to find JSON in shell output: {output}")
for (position, _) in output.match_indices('{') {
let candidate = &output[position..];
let mut deserializer = serde_json::Deserializer::from_str(candidate);
if let Ok(env_map) = HashMap::<String, String>::deserialize(&mut deserializer) {
return Ok(env_map);
}
}
anyhow::bail!("Failed to find JSON in shell output: {output}")
}
pub fn print_env() {
let env_vars: HashMap<String, String> = std::env::vars().collect();
let json = serde_json::to_string_pretty(&env_vars).unwrap_or_else(|err| {
eprintln!("Error serializing environment variables: {}", err);
std::process::exit(1);
});
println!("{}", json);
let env_vars: HashMap<String, String> = std::env::vars().collect();
let json = serde_json::to_string_pretty(&env_vars).unwrap_or_else(|err| {
eprintln!("Error serializing environment variables: {}", err);
std::process::exit(1);
});
println!("{}", json);
}
/// Capture all environment variables from the login shell in the given directory.
pub async fn capture(
shell_path: impl AsRef<Path>,
args: &[String],
directory: impl AsRef<Path>,
shell_path: impl AsRef<Path>,
args: &[String],
directory: impl AsRef<Path>,
) -> Result<collections::HashMap<String, String>> {
#[cfg(windows)]
return capture_windows(shell_path.as_ref(), args, directory.as_ref()).await;
#[cfg(unix)]
return capture_unix(shell_path.as_ref(), args, directory.as_ref()).await;
#[cfg(windows)]
return capture_windows(shell_path.as_ref(), args, directory.as_ref()).await;
#[cfg(unix)]
return capture_unix(shell_path.as_ref(), args, directory.as_ref()).await;
}
/// Try to parse the environment output before checking the exit status.
@@ -44,301 +44,301 @@ pub async fn capture(
/// non-zero exit status even though `zed --printenv` ran successfully and
/// produced valid output on its separate fd.
fn parse_env_output(
env_output: &str,
status: &std::process::ExitStatus,
successful_capture_warning: impl FnOnce() -> String,
failed_capture_error: impl FnOnce() -> String,
env_output: &str,
status: &std::process::ExitStatus,
successful_capture_warning: impl FnOnce() -> String,
failed_capture_error: impl FnOnce() -> String,
) -> Result<collections::HashMap<String, String>> {
match parse_env_map_from_noisy_output(env_output) {
Ok(env_map) => {
if !status.success() {
log::warn!("{}", successful_capture_warning());
}
Ok(env_map)
}
Err(parse_error) => {
if !status.success() {
anyhow::bail!(
"{}. Failed to deserialize environment variables from json: {parse_error}. output: {env_output}",
failed_capture_error(),
);
}
match parse_env_map_from_noisy_output(env_output) {
Ok(env_map) => {
if !status.success() {
log::warn!("{}", successful_capture_warning());
}
Ok(env_map)
}
Err(parse_error) => {
if !status.success() {
anyhow::bail!(
"{}. Failed to deserialize environment variables from json: {parse_error}. output: {env_output}",
failed_capture_error(),
);
}
anyhow::bail!(
"Failed to deserialize environment variables from json: {parse_error}. output: {env_output}"
);
}
}
anyhow::bail!(
"Failed to deserialize environment variables from json: {parse_error}. output: {env_output}"
);
}
}
}
#[cfg(unix)]
async fn capture_unix(
shell_path: &Path,
args: &[String],
directory: &Path,
shell_path: &Path,
args: &[String],
directory: &Path,
) -> Result<collections::HashMap<String, String>> {
use std::os::unix::process::CommandExt;
use std::os::unix::process::CommandExt;
use crate::command::new_std_command;
use crate::command::new_std_command;
let shell_kind = ShellKind::new(shell_path, false);
let quoted_zed_path = super::get_shell_safe_zed_path(shell_kind)?;
let shell_kind = ShellKind::new(shell_path, false);
let quoted_zed_path = super::get_shell_safe_zed_path(shell_kind)?;
let mut command_string = String::new();
let mut command = new_std_command(shell_path);
command.args(args);
// In some shells, file descriptors greater than 2 cannot be used in interactive mode,
// so file descriptor 0 (stdin) is used instead. This impacts zsh, old bash; perhaps others.
// See: https://github.com/zed-industries/zed/pull/32136#issuecomment-2999645482
const FD_STDIN: std::os::fd::RawFd = 0;
const FD_STDOUT: std::os::fd::RawFd = 1;
const FD_STDERR: std::os::fd::RawFd = 2;
let mut command_string = String::new();
let mut command = new_std_command(shell_path);
command.args(args);
// In some shells, file descriptors greater than 2 cannot be used in interactive mode,
// so file descriptor 0 (stdin) is used instead. This impacts zsh, old bash; perhaps others.
// See: https://github.com/zed-industries/zed/pull/32136#issuecomment-2999645482
const FD_STDIN: std::os::fd::RawFd = 0;
const FD_STDOUT: std::os::fd::RawFd = 1;
const FD_STDERR: std::os::fd::RawFd = 2;
let (fd_num, redir) = match shell_kind {
ShellKind::Rc => (FD_STDIN, format!(">[1={}]", FD_STDIN)), // `[1=0]`
ShellKind::Nushell | ShellKind::Tcsh => (FD_STDOUT, "".to_string()),
// xonsh doesn't support redirecting to stdin, and control sequences are printed to
// stdout on startup
ShellKind::Xonsh => (FD_STDERR, "o>e".to_string()),
ShellKind::PowerShell => (FD_STDIN, format!(">{}", FD_STDIN)),
_ => (FD_STDIN, format!(">&{}", FD_STDIN)), // `>&0`
};
let (fd_num, redir) = match shell_kind {
ShellKind::Rc => (FD_STDIN, format!(">[1={}]", FD_STDIN)), // `[1=0]`
ShellKind::Nushell | ShellKind::Tcsh => (FD_STDOUT, "".to_string()),
// xonsh doesn't support redirecting to stdin, and control sequences are printed to
// stdout on startup
ShellKind::Xonsh => (FD_STDERR, "o>e".to_string()),
ShellKind::PowerShell => (FD_STDIN, format!(">{}", FD_STDIN)),
_ => (FD_STDIN, format!(">&{}", FD_STDIN)), // `>&0`
};
match shell_kind {
ShellKind::Csh | ShellKind::Tcsh => {
// For csh/tcsh, login shell requires passing `-` as 0th argument (instead of `-l`)
command.arg0("-");
}
ShellKind::Fish => {
// in fish, asdf, direnv attach to the `fish_prompt` event
command_string.push_str("emit fish_prompt;");
command.arg("-l");
}
_ => {
command.arg("-l");
}
}
match shell_kind {
ShellKind::Csh | ShellKind::Tcsh => {
// For csh/tcsh, login shell requires passing `-` as 0th argument (instead of `-l`)
command.arg0("-");
}
ShellKind::Fish => {
// in fish, asdf, direnv attach to the `fish_prompt` event
command_string.push_str("emit fish_prompt;");
command.arg("-l");
}
_ => {
command.arg("-l");
}
}
match shell_kind {
// Nushell does not allow non-interactive login shells.
// Instead of doing "-l -i -c '<command>'"
// use "-l -e '<command>; exit'" instead
ShellKind::Nushell => command.arg("-e"),
_ => command.args(["-i", "-c"]),
};
match shell_kind {
// Nushell does not allow non-interactive login shells.
// Instead of doing "-l -i -c '<command>'"
// use "-l -e '<command>; exit'" instead
ShellKind::Nushell => command.arg("-e"),
_ => command.args(["-i", "-c"]),
};
// Prefix with "./" if the path starts with "-" to prevent cd from interpreting it as a flag
let dir_str = directory.to_string_lossy();
let dir_str = if dir_str.starts_with('-') {
format!("./{dir_str}").into()
} else {
dir_str
};
let quoted_dir = shell_kind
.try_quote(&dir_str)
.context("unexpected null in directory name")?;
// Prefix with "./" if the path starts with "-" to prevent cd from interpreting it as a flag
let dir_str = directory.to_string_lossy();
let dir_str = if dir_str.starts_with('-') {
format!("./{dir_str}").into()
} else {
dir_str
};
let quoted_dir = shell_kind
.try_quote(&dir_str)
.context("unexpected null in directory name")?;
// cd into the directory, triggering directory specific side-effects (asdf, direnv, etc)
command_string.push_str(&format!("cd {};", quoted_dir));
if let Some(prefix) = shell_kind.command_prefix() {
command_string.push(prefix);
}
command_string.push_str(&format!("{} --printenv {}", quoted_zed_path, redir));
// cd into the directory, triggering directory specific side-effects (asdf, direnv, etc)
command_string.push_str(&format!("cd {};", quoted_dir));
if let Some(prefix) = shell_kind.command_prefix() {
command_string.push(prefix);
}
command_string.push_str(&format!("{} --printenv {}", quoted_zed_path, redir));
if let ShellKind::Nushell = shell_kind {
command_string.push_str("; exit");
}
if let ShellKind::Nushell = shell_kind {
command_string.push_str("; exit");
}
command.arg(&command_string);
command.arg(&command_string);
super::set_pre_exec_to_start_new_session(&mut command);
super::set_pre_exec_to_start_new_session(&mut command);
let (env_output, process_output) = spawn_and_read_fd(command, fd_num).await?;
let env_output = String::from_utf8_lossy(&env_output);
let (env_output, process_output) = spawn_and_read_fd(command, fd_num).await?;
let env_output = String::from_utf8_lossy(&env_output);
parse_env_output(
&env_output,
&process_output.status,
|| {
format!(
"login shell exited with {} but environment was captured successfully. stderr: {:?}",
process_output.status,
String::from_utf8_lossy(&process_output.stderr),
)
},
|| {
format!(
"login shell exited with {}. stdout: {:?}, stderr: {:?}",
process_output.status,
String::from_utf8_lossy(&process_output.stdout),
String::from_utf8_lossy(&process_output.stderr),
)
},
)
parse_env_output(
&env_output,
&process_output.status,
|| {
format!(
"login shell exited with {} but environment was captured successfully. stderr: {:?}",
process_output.status,
String::from_utf8_lossy(&process_output.stderr),
)
},
|| {
format!(
"login shell exited with {}. stdout: {:?}, stderr: {:?}",
process_output.status,
String::from_utf8_lossy(&process_output.stdout),
String::from_utf8_lossy(&process_output.stderr),
)
},
)
}
#[cfg(unix)]
async fn spawn_and_read_fd(
mut command: std::process::Command,
child_fd: std::os::fd::RawFd,
mut command: std::process::Command,
child_fd: std::os::fd::RawFd,
) -> anyhow::Result<(Vec<u8>, std::process::Output)> {
use command_fds::{CommandFdExt, FdMapping};
use std::{io::Read, process::Stdio};
use command_fds::{CommandFdExt, FdMapping};
use std::{io::Read, process::Stdio};
let (mut reader, writer) = std::io::pipe()?;
let (mut reader, writer) = std::io::pipe()?;
command.fd_mappings(vec![FdMapping {
parent_fd: writer.into(),
child_fd,
}])?;
command.fd_mappings(vec![FdMapping {
parent_fd: writer.into(),
child_fd,
}])?;
let process = smol::process::Command::from(command)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let process = smol::process::Command::from(command)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer)?;
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer)?;
Ok((buffer, process.output().await?))
Ok((buffer, process.output().await?))
}
#[cfg(windows)]
async fn capture_windows(
shell_path: &Path,
args: &[String],
directory: &Path,
shell_path: &Path,
args: &[String],
directory: &Path,
) -> Result<collections::HashMap<String, String>> {
use std::process::Stdio;
use std::process::Stdio;
let zed_path =
std::env::current_exe().context("Failed to determine current zed executable path.")?;
let zed_path =
std::env::current_exe().context("Failed to determine current zed executable path.")?;
let shell_kind = ShellKind::new(shell_path, true);
// Prefix with "./" if the path starts with "-" to prevent cd from interpreting it as a flag
let directory_string = directory.display().to_string();
let directory_string = if directory_string.starts_with('-') {
format!("./{directory_string}")
} else {
directory_string
};
let zed_path_string = zed_path.display().to_string();
let quote_for_shell = |value: &str| {
shell_kind
.try_quote(value)
.map(|quoted| quoted.into_owned())
.context("unexpected null in directory name")
};
let mut cmd = crate::command::new_command(shell_path);
cmd.args(args);
let quoted_directory = quote_for_shell(&directory_string)?;
let quoted_zed_path = quote_for_shell(&zed_path_string)?;
let cmd = match shell_kind {
ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
| ShellKind::Fish
| ShellKind::Xonsh
| ShellKind::Posix => cmd.args([
"-l",
"-i",
"-c",
&format!("cd {}; {} --printenv", quoted_directory, quoted_zed_path),
]),
ShellKind::PowerShell | ShellKind::Pwsh => cmd.args([
"-NonInteractive",
"-NoProfile",
"-Command",
&format!(
"Set-Location {}; & {} --printenv",
quoted_directory, quoted_zed_path
),
]),
ShellKind::Elvish => cmd.args([
"-c",
&format!("cd {}; {} --printenv", quoted_directory, quoted_zed_path),
]),
ShellKind::Nushell => {
let zed_command = shell_kind
.prepend_command_prefix(&quoted_zed_path)
.into_owned();
cmd.args([
"-c",
&format!("cd {}; {} --printenv", quoted_directory, zed_command),
])
}
ShellKind::Cmd => {
let dir = directory_string.trim_end_matches('\\');
cmd.args(["/d", "/c", "cd", dir, "&&", &zed_path_string, "--printenv"])
}
}
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let output = cmd
.output()
.await
.with_context(|| format!("command {cmd:?}"))?;
let env_output = String::from_utf8_lossy(&output.stdout);
let shell_kind = ShellKind::new(shell_path, true);
// Prefix with "./" if the path starts with "-" to prevent cd from interpreting it as a flag
let directory_string = directory.display().to_string();
let directory_string = if directory_string.starts_with('-') {
format!("./{directory_string}")
} else {
directory_string
};
let zed_path_string = zed_path.display().to_string();
let quote_for_shell = |value: &str| {
shell_kind
.try_quote(value)
.map(|quoted| quoted.into_owned())
.context("unexpected null in directory name")
};
let mut cmd = crate::command::new_command(shell_path);
cmd.args(args);
let quoted_directory = quote_for_shell(&directory_string)?;
let quoted_zed_path = quote_for_shell(&zed_path_string)?;
let cmd = match shell_kind {
ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
| ShellKind::Fish
| ShellKind::Xonsh
| ShellKind::Posix => cmd.args([
"-l",
"-i",
"-c",
&format!("cd {}; {} --printenv", quoted_directory, quoted_zed_path),
]),
ShellKind::PowerShell | ShellKind::Pwsh => cmd.args([
"-NonInteractive",
"-NoProfile",
"-Command",
&format!(
"Set-Location {}; & {} --printenv",
quoted_directory, quoted_zed_path
),
]),
ShellKind::Elvish => cmd.args([
"-c",
&format!("cd {}; {} --printenv", quoted_directory, quoted_zed_path),
]),
ShellKind::Nushell => {
let zed_command = shell_kind
.prepend_command_prefix(&quoted_zed_path)
.into_owned();
cmd.args([
"-c",
&format!("cd {}; {} --printenv", quoted_directory, zed_command),
])
}
ShellKind::Cmd => {
let dir = directory_string.trim_end_matches('\\');
cmd.args(["/d", "/c", "cd", dir, "&&", &zed_path_string, "--printenv"])
}
}
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let output = cmd
.output()
.await
.with_context(|| format!("command {cmd:?}"))?;
let env_output = String::from_utf8_lossy(&output.stdout);
parse_env_output(
&env_output,
&output.status,
|| {
format!(
"Command {cmd:?} exited with {} but environment was captured successfully. stderr: {:?}",
output.status,
String::from_utf8_lossy(&output.stderr),
)
},
|| {
format!(
"Command {cmd:?} failed with {}. stdout: {:?}, stderr: {:?}",
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
)
},
)
parse_env_output(
&env_output,
&output.status,
|| {
format!(
"Command {cmd:?} exited with {} but environment was captured successfully. stderr: {:?}",
output.status,
String::from_utf8_lossy(&output.stderr),
)
},
|| {
format!(
"Command {cmd:?} failed with {}. stdout: {:?}, stderr: {:?}",
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
)
},
)
}
#[cfg(test)]
mod tests {
use std::process::ExitStatus;
use std::process::ExitStatus;
use super::*;
use super::*;
#[cfg(unix)]
fn exit_status(code: i32) -> ExitStatus {
use std::os::unix::process::ExitStatusExt;
#[cfg(unix)]
fn exit_status(code: i32) -> ExitStatus {
use std::os::unix::process::ExitStatusExt;
ExitStatus::from_raw(code << 8)
}
ExitStatus::from_raw(code << 8)
}
#[cfg(windows)]
fn exit_status(code: u32) -> ExitStatus {
use std::os::windows::process::ExitStatusExt;
#[cfg(windows)]
fn exit_status(code: u32) -> ExitStatus {
use std::os::windows::process::ExitStatusExt;
ExitStatus::from_raw(code)
}
ExitStatus::from_raw(code)
}
#[test]
fn parse_env_output_accepts_valid_env_when_shell_exits_nonzero() {
let env_json = serde_json::json!({
"PATH": "/usr/bin",
"SHELL": "/bin/zsh",
});
let env_output = format!("shell startup noise\n{env_json}\nshell shutdown noise");
#[test]
fn parse_env_output_accepts_valid_env_when_shell_exits_nonzero() {
let env_json = serde_json::json!({
"PATH": "/usr/bin",
"SHELL": "/bin/zsh",
});
let env_output = format!("shell startup noise\n{env_json}\nshell shutdown noise");
let env_map = parse_env_output(
&env_output,
&exit_status(1),
|| "shell exited with 1 but environment was captured successfully".to_string(),
|| panic!("failed capture error should not be evaluated for valid environment output"),
)
.expect("valid environment output should be returned despite non-zero shell exit");
assert_eq!(env_map.get("PATH").map(String::as_str), Some("/usr/bin"));
assert_eq!(env_map.get("SHELL").map(String::as_str), Some("/bin/zsh"));
}
let env_map = parse_env_output(
&env_output,
&exit_status(1),
|| "shell exited with 1 but environment was captured successfully".to_string(),
|| panic!("failed capture error should not be evaluated for valid environment output"),
)
.expect("valid environment output should be returned despite non-zero shell exit");
assert_eq!(env_map.get("PATH").map(String::as_str), Some("/usr/bin"));
assert_eq!(env_map.get("SHELL").map(String::as_str), Some("/bin/zsh"));
}
}
+38 -38
View File
@@ -1,46 +1,46 @@
pub fn format_file_size(size: u64, use_decimal: bool) -> String {
if use_decimal {
if size < 1000 {
format!("{size}B")
} else if size < 1000 * 1000 {
format!("{:.1}KB", size as f64 / 1000.0)
} else {
format!("{:.1}MB", size as f64 / (1000.0 * 1000.0))
}
} else if size < 1024 {
format!("{size}B")
} else if size < 1024 * 1024 {
format!("{:.1}KiB", size as f64 / 1024.0)
} else {
format!("{:.1}MiB", size as f64 / (1024.0 * 1024.0))
}
if use_decimal {
if size < 1000 {
format!("{size}B")
} else if size < 1000 * 1000 {
format!("{:.1}KB", size as f64 / 1000.0)
} else {
format!("{:.1}MB", size as f64 / (1000.0 * 1000.0))
}
} else if size < 1024 {
format!("{size}B")
} else if size < 1024 * 1024 {
format!("{:.1}KiB", size as f64 / 1024.0)
} else {
format!("{:.1}MiB", size as f64 / (1024.0 * 1024.0))
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::*;
#[test]
fn test_format_file_size_decimal() {
assert_eq!(format_file_size(0, true), "0B");
assert_eq!(format_file_size(999, true), "999B");
assert_eq!(format_file_size(1000, true), "1.0KB");
assert_eq!(format_file_size(1500, true), "1.5KB");
assert_eq!(format_file_size(999999, true), "1000.0KB");
assert_eq!(format_file_size(1000000, true), "1.0MB");
assert_eq!(format_file_size(1500000, true), "1.5MB");
assert_eq!(format_file_size(10000000, true), "10.0MB");
}
#[test]
fn test_format_file_size_decimal() {
assert_eq!(format_file_size(0, true), "0B");
assert_eq!(format_file_size(999, true), "999B");
assert_eq!(format_file_size(1000, true), "1.0KB");
assert_eq!(format_file_size(1500, true), "1.5KB");
assert_eq!(format_file_size(999999, true), "1000.0KB");
assert_eq!(format_file_size(1000000, true), "1.0MB");
assert_eq!(format_file_size(1500000, true), "1.5MB");
assert_eq!(format_file_size(10000000, true), "10.0MB");
}
#[test]
fn test_format_file_size_binary() {
assert_eq!(format_file_size(0, false), "0B");
assert_eq!(format_file_size(1023, false), "1023B");
assert_eq!(format_file_size(1024, false), "1.0KiB");
assert_eq!(format_file_size(1536, false), "1.5KiB");
assert_eq!(format_file_size(1048575, false), "1024.0KiB");
assert_eq!(format_file_size(1048576, false), "1.0MiB");
assert_eq!(format_file_size(1572864, false), "1.5MiB");
assert_eq!(format_file_size(10485760, false), "10.0MiB");
}
#[test]
fn test_format_file_size_binary() {
assert_eq!(format_file_size(0, false), "0B");
assert_eq!(format_file_size(1023, false), "1023B");
assert_eq!(format_file_size(1024, false), "1.0KiB");
assert_eq!(format_file_size(1536, false), "1.5KiB");
assert_eq!(format_file_size(1048575, false), "1024.0KiB");
assert_eq!(format_file_size(1048576, false), "1.0MiB");
assert_eq!(format_file_size(1572864, false), "1.5MiB");
assert_eq!(format_file_size(10485760, false), "10.0MiB");
}
}
+53 -53
View File
@@ -10,71 +10,71 @@ use std::path::{Path, PathBuf};
use tempfile::TempDir;
pub struct TempTree {
_temp_dir: TempDir,
path: PathBuf,
_temp_dir: TempDir,
path: PathBuf,
}
impl TempTree {
pub fn new(tree: serde_json::Value) -> Self {
let dir = TempDir::new().unwrap();
let path = std::fs::canonicalize(dir.path()).unwrap();
write_tree(path.as_path(), tree);
pub fn new(tree: serde_json::Value) -> Self {
let dir = TempDir::new().unwrap();
let path = std::fs::canonicalize(dir.path()).unwrap();
write_tree(path.as_path(), tree);
Self {
_temp_dir: dir,
path,
}
}
Self {
_temp_dir: dir,
path,
}
}
pub fn path(&self) -> &Path {
self.path.as_path()
}
pub fn path(&self) -> &Path {
self.path.as_path()
}
}
fn write_tree(path: &Path, tree: serde_json::Value) {
use serde_json::Value;
use std::fs;
use serde_json::Value;
use std::fs;
if let Value::Object(map) = tree {
for (name, contents) in map {
let mut path = PathBuf::from(path);
path.push(name);
match contents {
Value::Object(_) => {
fs::create_dir(&path).unwrap();
if let Value::Object(map) = tree {
for (name, contents) in map {
let mut path = PathBuf::from(path);
path.push(name);
match contents {
Value::Object(_) => {
fs::create_dir(&path).unwrap();
#[cfg(not(target_family = "wasm"))]
if path.file_name() == Some(OsStr::new(".git")) {
git2::Repository::init(path.parent().unwrap()).unwrap();
}
#[cfg(not(target_family = "wasm"))]
if path.file_name() == Some(OsStr::new(".git")) {
git2::Repository::init(path.parent().unwrap()).unwrap();
}
write_tree(&path, contents);
}
Value::Null => {
fs::create_dir(&path).unwrap();
}
Value::String(contents) => {
fs::write(&path, contents).unwrap();
}
_ => {
panic!("JSON object must contain only objects, strings, or null");
}
}
}
} else {
panic!("You must pass a JSON object to this helper")
}
write_tree(&path, contents);
}
Value::Null => {
fs::create_dir(&path).unwrap();
}
Value::String(contents) => {
fs::write(&path, contents).unwrap();
}
_ => {
panic!("JSON object must contain only objects, strings, or null");
}
}
}
} else {
panic!("You must pass a JSON object to this helper")
}
}
pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String {
let mut text = String::new();
for row in 0..rows {
let c: char = (start_char as u32 + row as u32) as u8 as char;
let mut line = c.to_string().repeat(cols);
if row < rows - 1 {
line.push('\n');
}
text += &line;
}
text
let mut text = String::new();
for row in 0..rows {
let c: char = (start_char as u32 + row as u32) as u8 as char;
let mut line = c.to_string().repeat(cols);
if row < rows - 1 {
line.push('\n');
}
text += &line;
}
text
}
+49 -43
View File
@@ -1,62 +1,68 @@
pub enum SetEqError<T> {
LeftMissing(T),
RightMissing(T),
LeftMissing(T),
RightMissing(T),
}
impl<T> SetEqError<T> {
pub fn map<R, F: FnOnce(T) -> R>(self, update: F) -> SetEqError<R> {
match self {
SetEqError::LeftMissing(missing) => SetEqError::LeftMissing(update(missing)),
SetEqError::RightMissing(missing) => SetEqError::RightMissing(update(missing)),
}
}
pub fn map<R, F: FnOnce(T) -> R>(self, update: F) -> SetEqError<R> {
match self {
SetEqError::LeftMissing(missing) => SetEqError::LeftMissing(update(missing)),
SetEqError::RightMissing(missing) => SetEqError::RightMissing(update(missing)),
}
}
}
#[macro_export]
macro_rules! set_eq {
($left:expr,$right:expr) => {{
use util::test::*;
($left:expr,$right:expr) => {{
use util::test::*;
let left = $left;
let right = $right;
let left = $left;
let right = $right;
let mut result = Ok(());
for right_value in right.iter() {
if !left.contains(right_value) {
result = Err(SetEqError::LeftMissing(right_value.clone()));
break;
}
}
let mut result = Ok(());
for right_value in right.iter() {
if !left.contains(right_value) {
result = Err(SetEqError::LeftMissing(right_value.clone()));
break;
}
}
if result.is_ok() {
for left_value in left.iter() {
if !right.contains(left_value) {
result = Err(SetEqError::RightMissing(left_value.clone()));
}
}
}
if result.is_ok() {
for left_value in left.iter() {
if !right.contains(left_value) {
result = Err(SetEqError::RightMissing(left_value.clone()));
}
}
}
result
}};
result
}};
}
#[macro_export]
macro_rules! assert_set_eq {
($left:expr,$right:expr) => {{
use util::test::*;
use util::set_eq;
($left:expr,$right:expr) => {{
use util::set_eq;
use util::test::*;
let left = $left;
let right = $right;
let left = $left;
let right = $right;
match set_eq!(&left, &right) {
Err(SetEqError::LeftMissing(missing)) => {
panic!("assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nleft does not contain {:?}", &left, &right, &missing);
},
Err(SetEqError::RightMissing(missing)) => {
panic!("assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nright does not contain {:?}", &left, &right, &missing);
},
_ => {}
}
}};
match set_eq!(&left, &right) {
Err(SetEqError::LeftMissing(missing)) => {
panic!(
"assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nleft does not contain {:?}",
&left, &right, &missing
);
}
Err(SetEqError::RightMissing(missing)) => {
panic!(
"assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nright does not contain {:?}",
&left, &right, &missing
);
}
_ => {}
}
}};
}
+192 -192
View File
@@ -4,22 +4,22 @@ use std::{cmp::Ordering, ops::Range};
/// Construct a string and a list of offsets within that string using a single
/// string containing embedded position markers.
pub fn marked_text_offsets_by(
marked_text: &str,
markers: Vec<char>,
marked_text: &str,
markers: Vec<char>,
) -> (String, HashMap<char, Vec<usize>>) {
let mut extracted_markers: HashMap<char, Vec<usize>> = Default::default();
let mut unmarked_text = String::new();
let mut extracted_markers: HashMap<char, Vec<usize>> = Default::default();
let mut unmarked_text = String::new();
for char in marked_text.chars() {
if markers.contains(&char) {
let char_offsets = extracted_markers.entry(char).or_default();
char_offsets.push(unmarked_text.len());
} else {
unmarked_text.push(char);
}
}
for char in marked_text.chars() {
if markers.contains(&char) {
let char_offsets = extracted_markers.entry(char).or_default();
char_offsets.push(unmarked_text.len());
} else {
unmarked_text.push(char);
}
}
(unmarked_text, extracted_markers)
(unmarked_text, extracted_markers)
}
/// Construct a string and a list of ranges within that string using a single
@@ -29,56 +29,56 @@ pub fn marked_text_offsets_by(
///
/// The returned ranges will be grouped by their range marking characters.
pub fn marked_text_ranges_by(
marked_text: &str,
markers: Vec<TextRangeMarker>,
marked_text: &str,
markers: Vec<TextRangeMarker>,
) -> (String, HashMap<TextRangeMarker, Vec<Range<usize>>>) {
let all_markers = markers.iter().flat_map(|m| m.markers()).collect();
let all_markers = markers.iter().flat_map(|m| m.markers()).collect();
let (unmarked_text, mut marker_offsets) = marked_text_offsets_by(marked_text, all_markers);
let range_lookup = markers
.into_iter()
.map(|marker| {
(
marker.clone(),
match marker {
TextRangeMarker::Empty(empty_marker_char) => marker_offsets
.remove(&empty_marker_char)
.unwrap_or_default()
.into_iter()
.map(|empty_index| empty_index..empty_index)
.collect::<Vec<Range<usize>>>(),
TextRangeMarker::Range(start_marker, end_marker) => {
let starts = marker_offsets.remove(&start_marker).unwrap_or_default();
let ends = marker_offsets.remove(&end_marker).unwrap_or_default();
assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced");
starts
.into_iter()
.zip(ends)
.map(|(start, end)| {
assert!(end >= start, "marked ranges must be disjoint");
start..end
})
.collect::<Vec<Range<usize>>>()
}
TextRangeMarker::ReverseRange(start_marker, end_marker) => {
let starts = marker_offsets.remove(&start_marker).unwrap_or_default();
let ends = marker_offsets.remove(&end_marker).unwrap_or_default();
assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced");
starts
.into_iter()
.zip(ends)
.map(|(start, end)| {
assert!(end >= start, "marked ranges must be disjoint");
end..start
})
.collect::<Vec<Range<usize>>>()
}
},
)
})
.collect();
let (unmarked_text, mut marker_offsets) = marked_text_offsets_by(marked_text, all_markers);
let range_lookup = markers
.into_iter()
.map(|marker| {
(
marker.clone(),
match marker {
TextRangeMarker::Empty(empty_marker_char) => marker_offsets
.remove(&empty_marker_char)
.unwrap_or_default()
.into_iter()
.map(|empty_index| empty_index..empty_index)
.collect::<Vec<Range<usize>>>(),
TextRangeMarker::Range(start_marker, end_marker) => {
let starts = marker_offsets.remove(&start_marker).unwrap_or_default();
let ends = marker_offsets.remove(&end_marker).unwrap_or_default();
assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced");
starts
.into_iter()
.zip(ends)
.map(|(start, end)| {
assert!(end >= start, "marked ranges must be disjoint");
start..end
})
.collect::<Vec<Range<usize>>>()
}
TextRangeMarker::ReverseRange(start_marker, end_marker) => {
let starts = marker_offsets.remove(&start_marker).unwrap_or_default();
let ends = marker_offsets.remove(&end_marker).unwrap_or_default();
assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced");
starts
.into_iter()
.zip(ends)
.map(|(start, end)| {
assert!(end >= start, "marked ranges must be disjoint");
end..start
})
.collect::<Vec<Range<usize>>>()
}
},
)
})
.collect();
(unmarked_text, range_lookup)
(unmarked_text, range_lookup)
}
/// Construct a string and a list of ranges within that string using a single
@@ -111,171 +111,171 @@ pub fn marked_text_ranges_by(
/// source code.
#[track_caller]
pub fn marked_text_ranges(
marked_text: &str,
ranges_are_directed: bool,
marked_text: &str,
ranges_are_directed: bool,
) -> (String, Vec<Range<usize>>) {
let mut unmarked_text = String::with_capacity(marked_text.len());
let mut ranges = Vec::new();
let mut prev_marked_ix = 0;
let mut current_range_start = None;
let mut current_range_cursor = None;
let mut unmarked_text = String::with_capacity(marked_text.len());
let mut ranges = Vec::new();
let mut prev_marked_ix = 0;
let mut current_range_start = None;
let mut current_range_cursor = None;
let marked_text = marked_text.replace('•', " ");
for (marked_ix, marker) in marked_text.match_indices(&['«', '»', 'ˇ']) {
unmarked_text.push_str(&marked_text[prev_marked_ix..marked_ix]);
let unmarked_len = unmarked_text.len();
let len = marker.len();
prev_marked_ix = marked_ix + len;
let marked_text = marked_text.replace('•', " ");
for (marked_ix, marker) in marked_text.match_indices(&['«', '»', 'ˇ']) {
unmarked_text.push_str(&marked_text[prev_marked_ix..marked_ix]);
let unmarked_len = unmarked_text.len();
let len = marker.len();
prev_marked_ix = marked_ix + len;
match marker {
"ˇ" => {
if current_range_start.is_some() {
if current_range_cursor.is_some() {
panic!("duplicate point marker 'ˇ' at index {marked_ix}");
}
match marker {
"ˇ" => {
if current_range_start.is_some() {
if current_range_cursor.is_some() {
panic!("duplicate point marker 'ˇ' at index {marked_ix}");
}
current_range_cursor = Some(unmarked_len);
} else {
ranges.push(unmarked_len..unmarked_len);
}
}
"«" => {
if current_range_start.is_some() {
panic!("unexpected range start marker '«' at index {marked_ix}");
}
current_range_start = Some(unmarked_len);
}
"»" => {
let current_range_start = if let Some(start) = current_range_start.take() {
start
} else {
panic!("unexpected range end marker '»' at index {marked_ix}");
};
current_range_cursor = Some(unmarked_len);
} else {
ranges.push(unmarked_len..unmarked_len);
}
}
"«" => {
if current_range_start.is_some() {
panic!("unexpected range start marker '«' at index {marked_ix}");
}
current_range_start = Some(unmarked_len);
}
"»" => {
let current_range_start = if let Some(start) = current_range_start.take() {
start
} else {
panic!("unexpected range end marker '»' at index {marked_ix}");
};
let mut reversed = false;
if let Some(current_range_cursor) = current_range_cursor.take() {
if current_range_cursor == current_range_start {
reversed = true;
} else if current_range_cursor != unmarked_len {
panic!("unexpected 'ˇ' marker in the middle of a range");
}
} else if ranges_are_directed {
panic!("missing 'ˇ' marker to indicate range direction");
}
let mut reversed = false;
if let Some(current_range_cursor) = current_range_cursor.take() {
if current_range_cursor == current_range_start {
reversed = true;
} else if current_range_cursor != unmarked_len {
panic!("unexpected 'ˇ' marker in the middle of a range");
}
} else if ranges_are_directed {
panic!("missing 'ˇ' marker to indicate range direction");
}
ranges.push(if reversed {
unmarked_len..current_range_start
} else {
current_range_start..unmarked_len
});
}
_ => unreachable!(),
}
}
ranges.push(if reversed {
unmarked_len..current_range_start
} else {
current_range_start..unmarked_len
});
}
_ => unreachable!(),
}
}
unmarked_text.push_str(&marked_text[prev_marked_ix..]);
(unmarked_text, ranges)
unmarked_text.push_str(&marked_text[prev_marked_ix..]);
(unmarked_text, ranges)
}
#[track_caller]
pub fn marked_text_offsets(marked_text: &str) -> (String, Vec<usize>) {
let (text, ranges) = marked_text_ranges(marked_text, false);
(
text,
ranges
.into_iter()
.map(|range| {
assert_eq!(range.start, range.end);
range.start
})
.collect(),
)
let (text, ranges) = marked_text_ranges(marked_text, false);
(
text,
ranges
.into_iter()
.map(|range| {
assert_eq!(range.start, range.end);
range.start
})
.collect(),
)
}
pub fn generate_marked_text(
unmarked_text: &str,
ranges: &[Range<usize>],
indicate_cursors: bool,
unmarked_text: &str,
ranges: &[Range<usize>],
indicate_cursors: bool,
) -> String {
let mut marked_text = unmarked_text.to_string();
for range in ranges.iter().rev() {
if indicate_cursors {
match range.start.cmp(&range.end) {
Ordering::Less => {
marked_text.insert_str(range.end, "ˇ»");
marked_text.insert(range.start, '«');
}
Ordering::Equal => {
marked_text.insert(range.start, 'ˇ');
}
Ordering::Greater => {
marked_text.insert(range.start, '»');
marked_text.insert_str(range.end, "«ˇ");
}
}
} else {
match range.start.cmp(&range.end) {
Ordering::Equal => {
marked_text.insert(range.start, 'ˇ');
}
_ => {
marked_text.insert(range.end, '»');
marked_text.insert(range.start, '«');
}
}
}
}
marked_text
let mut marked_text = unmarked_text.to_string();
for range in ranges.iter().rev() {
if indicate_cursors {
match range.start.cmp(&range.end) {
Ordering::Less => {
marked_text.insert_str(range.end, "ˇ»");
marked_text.insert(range.start, '«');
}
Ordering::Equal => {
marked_text.insert(range.start, 'ˇ');
}
Ordering::Greater => {
marked_text.insert(range.start, '»');
marked_text.insert_str(range.end, "«ˇ");
}
}
} else {
match range.start.cmp(&range.end) {
Ordering::Equal => {
marked_text.insert(range.start, 'ˇ');
}
_ => {
marked_text.insert(range.end, '»');
marked_text.insert(range.start, '«');
}
}
}
}
marked_text
}
#[derive(Clone, Eq, PartialEq, Hash)]
pub enum TextRangeMarker {
Empty(char),
Range(char, char),
ReverseRange(char, char),
Empty(char),
Range(char, char),
ReverseRange(char, char),
}
impl TextRangeMarker {
fn markers(&self) -> Vec<char> {
match self {
Self::Empty(m) => vec![*m],
Self::Range(l, r) => vec![*l, *r],
Self::ReverseRange(l, r) => vec![*l, *r],
}
}
fn markers(&self) -> Vec<char> {
match self {
Self::Empty(m) => vec![*m],
Self::Range(l, r) => vec![*l, *r],
Self::ReverseRange(l, r) => vec![*l, *r],
}
}
}
impl From<char> for TextRangeMarker {
fn from(marker: char) -> Self {
Self::Empty(marker)
}
fn from(marker: char) -> Self {
Self::Empty(marker)
}
}
impl From<(char, char)> for TextRangeMarker {
fn from((left_marker, right_marker): (char, char)) -> Self {
Self::Range(left_marker, right_marker)
}
fn from((left_marker, right_marker): (char, char)) -> Self {
Self::Range(left_marker, right_marker)
}
}
#[cfg(test)]
mod tests {
use super::{generate_marked_text, marked_text_ranges};
use super::{generate_marked_text, marked_text_ranges};
#[allow(clippy::reversed_empty_ranges)]
#[test]
fn test_marked_text() {
let (text, ranges) = marked_text_ranges("one «ˇtwo» «threeˇ» «ˇfour» fiveˇ six", true);
#[allow(clippy::reversed_empty_ranges)]
#[test]
fn test_marked_text() {
let (text, ranges) = marked_text_ranges("one «ˇtwo» «threeˇ» «ˇfour» fiveˇ six", true);
assert_eq!(text, "one two three four five six");
assert_eq!(ranges.len(), 4);
assert_eq!(ranges[0], 7..4);
assert_eq!(ranges[1], 8..13);
assert_eq!(ranges[2], 18..14);
assert_eq!(ranges[3], 23..23);
assert_eq!(text, "one two three four five six");
assert_eq!(ranges.len(), 4);
assert_eq!(ranges[0], 7..4);
assert_eq!(ranges[1], 8..13);
assert_eq!(ranges[2], 18..14);
assert_eq!(ranges[3], 23..23);
assert_eq!(
generate_marked_text(&text, &ranges, true),
"one «ˇtwo» «threeˇ» «ˇfour» fiveˇ six"
);
}
assert_eq!(
generate_marked_text(&text, &ranges, true),
"one «ˇtwo» «threeˇ» «ˇfour» fiveˇ six"
);
}
}
+23 -23
View File
@@ -1,33 +1,33 @@
use std::time::Duration;
pub fn duration_alt_display(duration: Duration) -> String {
let hours = duration.as_secs() / 3600;
let minutes = (duration.as_secs() % 3600) / 60;
let seconds = duration.as_secs() % 60;
let hours = duration.as_secs() / 3600;
let minutes = (duration.as_secs() % 3600) / 60;
let seconds = duration.as_secs() % 60;
if hours > 0 {
format!("{hours}h {minutes}m {seconds}s")
} else if minutes > 0 {
format!("{minutes}m {seconds}s")
} else {
format!("{seconds}s")
}
if hours > 0 {
format!("{hours}h {minutes}m {seconds}s")
} else if minutes > 0 {
format!("{minutes}m {seconds}s")
} else {
format!("{seconds}s")
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::*;
#[test]
fn test_duration_alt_display() {
use duration_alt_display as f;
assert_eq!("0s", f(Duration::from_secs(0)));
assert_eq!("59s", f(Duration::from_secs(59)));
assert_eq!("1m 0s", f(Duration::from_secs(60)));
assert_eq!("10m 0s", f(Duration::from_secs(600)));
assert_eq!("1h 0m 0s", f(Duration::from_secs(3600)));
assert_eq!("3h 2m 1s", f(Duration::from_secs(3600 * 3 + 60 * 2 + 1)));
assert_eq!("23h 59m 59s", f(Duration::from_secs(3600 * 24 - 1)));
assert_eq!("100h 0m 0s", f(Duration::from_secs(3600 * 100)));
}
#[test]
fn test_duration_alt_display() {
use duration_alt_display as f;
assert_eq!("0s", f(Duration::from_secs(0)));
assert_eq!("59s", f(Duration::from_secs(59)));
assert_eq!("1m 0s", f(Duration::from_secs(60)));
assert_eq!("10m 0s", f(Duration::from_secs(600)));
assert_eq!("1h 0m 0s", f(Duration::from_secs(3600)));
assert_eq!("3h 2m 1s", f(Duration::from_secs(3600 * 3 + 60 * 2 + 1)));
assert_eq!("23h 59m 59s", f(Duration::from_secs(3600 * 24 - 1)));
assert_eq!("100h 0m 0s", f(Duration::from_secs(3600 * 100)));
}
}
File diff suppressed because it is too large Load Diff