magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::*;

#[cfg(unix)]
pub(super) struct ValidatedDestination {
    pub(super) path: PathBuf,
    pub(super) file_name: std::ffi::OsString,
    pub(super) directory: File,
}

#[cfg(unix)]
pub(super) struct TemporaryArchiveGuard {
    pub(super) directory: File,
    pub(super) name: Option<std::ffi::OsString>,
}

#[cfg(unix)]
impl TemporaryArchiveGuard {
    pub(super) fn new(directory: &File, name: std::ffi::OsString) -> anyhow::Result<Self> {
        Ok(Self {
            directory: directory.try_clone()?,
            name: Some(name),
        })
    }

    fn disarm(&mut self) {
        self.name = None;
    }

    fn cleanup(&mut self) -> anyhow::Result<()> {
        let Some(name) = self.name.take() else {
            return Ok(());
        };
        unlink_at(&self.directory, &name)
    }
}

#[cfg(unix)]
impl Drop for TemporaryArchiveGuard {
    fn drop(&mut self) {
        let _ = self.cleanup();
    }
}
#[cfg(unix)]
pub(super) fn validate_destination(destination: &Path) -> anyhow::Result<ValidatedDestination> {
    let file_name = destination
        .file_name()
        .filter(|name| *name != std::ffi::OsStr::new("."))
        .ok_or_else(|| anyhow::anyhow!("session export destination must name a file"))?
        .to_os_string();
    let parent = destination.parent().unwrap_or_else(|| Path::new("."));
    validate_destination_parent(parent)?;
    let directory = open_directory_fd(parent)?;
    let path = parent.join(&file_name);
    match fs::symlink_metadata(&path) {
        Ok(_) => anyhow::bail!(
            "session export destination already exists: {}",
            path.display()
        ),
        Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(ValidatedDestination {
            path,
            file_name,
            directory,
        }),
        Err(error) => Err(anyhow::anyhow!(
            "could not inspect session export destination {}: {error}",
            path.display()
        )),
    }
}

#[cfg(unix)]
fn validate_destination_parent(parent: &Path) -> anyhow::Result<()> {
    let metadata = fs::symlink_metadata(parent)?;
    if metadata.file_type().is_symlink() || !metadata.file_type().is_dir() {
        anyhow::bail!("session export destination parent must be a directory")
    }
    {
        use std::os::unix::fs::MetadataExt;
        // SAFETY: geteuid has no preconditions and only reads process identity.
        let user_id = unsafe { libc::geteuid() };
        if metadata.uid() != user_id {
            anyhow::bail!("session export destination parent owner is not current user")
        }
        if metadata.mode() & 0o022 != 0 {
            anyhow::bail!("session export destination parent is writable by group or other users")
        }
    }
    Ok(())
}

#[cfg(unix)]
fn open_directory_fd(path: &Path) -> anyhow::Result<File> {
    use std::os::unix::fs::OpenOptionsExt;
    let mut options = OpenOptions::new();
    options
        .read(true)
        .custom_flags(libc::O_DIRECTORY | libc::O_NOFOLLOW);
    Ok(options.open(path)?)
}

#[cfg(unix)]
fn unlink_at(directory: &File, name: &std::ffi::OsStr) -> anyhow::Result<()> {
    use std::os::unix::ffi::OsStrExt;
    let name = std::ffi::CString::new(name.as_bytes())?;
    // SAFETY: directory is an open directory; name is NUL-free and points to valid bytes.
    let result = unsafe { libc::unlinkat(directory.as_raw_fd(), name.as_ptr(), 0) };
    if result == 0 || io::Error::last_os_error().kind() == io::ErrorKind::NotFound {
        Ok(())
    } else {
        Err(io::Error::last_os_error().into())
    }
}

#[cfg(unix)]
fn link_at(
    directory: &File,
    source: &std::ffi::OsStr,
    destination: &std::ffi::OsStr,
) -> io::Result<()> {
    use std::os::unix::ffi::OsStrExt;
    let source = std::ffi::CString::new(source.as_bytes())
        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "NUL in temporary name"))?;
    let destination = std::ffi::CString::new(destination.as_bytes())
        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "NUL in destination name"))?;
    // SAFETY: directory is retained open, and both names are NUL-free relative names.
    let result = unsafe {
        libc::linkat(
            directory.as_raw_fd(),
            source.as_ptr(),
            directory.as_raw_fd(),
            destination.as_ptr(),
            0,
        )
    };
    if result == 0 {
        Ok(())
    } else {
        Err(io::Error::last_os_error())
    }
}

#[cfg(unix)]
fn create_temporary_archive(directory: &File) -> anyhow::Result<(File, std::ffi::OsString)> {
    for _ in 0..SESSION_EXPORT_TEMP_ATTEMPTS {
        let sequence = NEXT_EXPORT_TEMP.fetch_add(1, Ordering::Relaxed);
        let name = std::ffi::OsString::from(format!(
            ".magi-code-session-export-{}-{sequence}.tmp",
            std::process::id()
        ));
        match openat_file(directory, &name) {
            Ok(file) => return Ok((file, name)),
            Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
            Err(error) => return Err(error.into()),
        }
    }
    anyhow::bail!("could not allocate a temporary session export archive")
}

#[cfg(unix)]
fn openat_file(directory: &File, name: &std::ffi::OsStr) -> io::Result<File> {
    use std::os::unix::{
        ffi::OsStrExt,
        io::{AsRawFd, FromRawFd},
    };
    let c_name = std::ffi::CString::new(name.as_bytes())
        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "NUL in name"))?;
    // SAFETY: directory is retained open and name is a NUL-free single path component.
    let fd = unsafe {
        libc::openat(
            directory.as_raw_fd(),
            c_name.as_ptr(),
            libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_NOFOLLOW | libc::O_CLOEXEC,
            0o600,
        )
    };
    if fd < 0 {
        return Err(io::Error::last_os_error());
    }
    // SAFETY: fd is uniquely owned from openat on success.
    Ok(unsafe { File::from_raw_fd(fd) })
}

#[cfg(unix)]
fn serialize_manifest(manifest: &ExportManifest, omissions: &[String]) -> anyhow::Result<Vec<u8>> {
    let document = ExportManifestDocument {
        schema: "magi-code.session_export",
        schema_version: 1,
        root_session_id: manifest.root_session_id.clone(),
        member_count: manifest.members.len(),
        source_bytes: manifest.source_bytes,
        members: manifest
            .members
            .iter()
            .map(|member| ExportManifestMember {
                path: member.archive_name.clone(),
                bytes: member.length,
                sha256: member.sha256.clone(),
            })
            .collect(),
        relationships: manifest.relationships.iter().cloned().collect(),
        completeness: ExportCompleteness {
            complete: omissions.is_empty(),
            omissions: omissions.to_vec(),
        },
    };
    let mut bytes = serde_json::to_vec_pretty(&document)?;
    bytes.push(b'\n');
    Ok(bytes)
}

#[cfg(unix)]
pub(super) fn write_archive(
    manifest: &mut ExportManifest,
    omissions: &[String],
    destination: &ValidatedDestination,
    cancellation: &AgentCancellation,
    warnings: &mut Vec<String>,
) -> anyhow::Result<()> {
    let (temporary, temporary_name) = create_temporary_archive(&destination.directory)?;
    let mut cleanup = TemporaryArchiveGuard::new(&destination.directory, temporary_name.clone())?;
    let result = write_archive_inner(
        manifest,
        omissions,
        destination,
        temporary,
        &temporary_name,
        &mut cleanup,
        cancellation,
        warnings,
    );
    match result {
        Ok(()) => Ok(()),
        Err(error) => match cleanup.cleanup() {
            Ok(()) => Err(error),
            Err(cleanup_error) => Err(anyhow::anyhow!(
                "{error}; session export cleanup diagnostic: {cleanup_error}"
            )),
        },
    }
}

#[expect(clippy::too_many_arguments)]
#[cfg(unix)]
fn write_archive_inner(
    manifest: &mut ExportManifest,
    omissions: &[String],
    destination: &ValidatedDestination,
    temporary: File,
    temporary_name: &std::ffi::OsStr,
    cleanup: &mut TemporaryArchiveGuard,
    cancellation: &AgentCancellation,
    warnings: &mut Vec<String>,
) -> anyhow::Result<()> {
    let mut archive = ZipWriter::new(temporary);
    let options = SimpleFileOptions::default()
        .compression_method(CompressionMethod::Deflated)
        .unix_permissions(SESSION_FILE_MODE);
    cancellation.check()?;
    manifest
        .members
        .sort_by(|left, right| left.archive_name.cmp(&right.archive_name));
    let manifest_bytes = serialize_manifest(manifest, omissions)?;
    archive.start_file("manifest.json", options)?;
    cancellation.check()?;
    archive.write_all(&manifest_bytes)?;
    for member in &mut manifest.members {
        cancellation.check()?;
        cancellation.check()?;
        archive.start_file(member.archive_name.clone(), options)?;
        member.file.seek(SeekFrom::Start(0))?;
        let mut source = (&mut member.file).take(member.length);
        let mut hasher = Sha256::new();
        let mut copied = 0_u64;
        let mut buffer = [0_u8; 64 * 1024];
        loop {
            cancellation.check()?;
            let read = source.read(&mut buffer)?;
            if read == 0 {
                break;
            }
            archive.write_all(&buffer[..read])?;
            hasher.update(&buffer[..read]);
            copied = copied
                .checked_add(read as u64)
                .ok_or_else(|| anyhow::anyhow!("session export source-byte count overflowed"))?;
        }
        if copied != member.length
            || member.file.metadata()?.len() != member.length
            || lower_hex(hasher.finalize()) != member.sha256
        {
            anyhow::bail!(
                "session export source changed while reading {}",
                member.archive_name
            )
        }
    }
    cancellation.check()?;
    let mut temporary = archive.finish()?;
    temporary.flush()?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        temporary.set_permissions(fs::Permissions::from_mode(SESSION_FILE_MODE))?;
    }
    temporary.sync_all()?;
    drop(temporary);
    cancellation.check()?;
    match link_at(
        &destination.directory,
        temporary_name,
        &destination.file_name,
    ) {
        Ok(()) => {}
        Err(error) if error.kind() == io::ErrorKind::AlreadyExists => anyhow::bail!(
            "session export destination already exists: {}",
            destination.path.display()
        ),
        Err(error) => {
            return Err(anyhow::anyhow!(
                "could not publish session export {}: {error}",
                destination.path.display()
            ));
        }
    }
    // The archive is committed once linkat succeeds. Cleanup and directory
    // durability failures are warnings only; cleanup consumes the guard name,
    // so Drop cannot retry it after publication.
    let cleanup_result = cleanup.cleanup();
    cleanup.disarm();
    if let Err(error) = cleanup_result {
        warnings.push(bounded_warning(&format!(
            "published archive temporary file cleanup failed; sensitive temporary data may remain: {error}"
        )));
    }
    if let Err(error) = destination.directory.sync_all() {
        warnings.push(bounded_warning(&format!(
            "published archive directory durability uncertain: {error}"
        )));
    }
    Ok(())
}