Skip to main content

verifyos_cli/parsers/
xcworkspace_parser.rs

1use miette::Diagnostic;
2use std::path::{Path, PathBuf};
3use thiserror::Error;
4
5#[derive(Debug, Error, Diagnostic)]
6pub enum WorkspaceError {
7    #[error("Failed to read Xcode workspace at {path}")]
8    ReadError { path: String, description: String },
9    #[error("Missing contents.xcworkspacedata in workspace {path}")]
10    MissingContents { path: String },
11}
12
13#[derive(Debug, Clone)]
14pub struct Xcworkspace {
15    pub project_paths: Vec<PathBuf>,
16}
17
18impl Xcworkspace {
19    pub fn from_path(path: impl AsRef<Path>) -> Result<Self, WorkspaceError> {
20        let path = path.as_ref();
21        let contents_path = if path
22            .extension()
23            .is_some_and(|ext| ext.eq_ignore_ascii_case("xcworkspacedata"))
24        {
25            path.to_path_buf()
26        } else {
27            let workspace_root = path;
28            let contents = workspace_root.join("contents.xcworkspacedata");
29            if !contents.exists() {
30                return Err(WorkspaceError::MissingContents {
31                    path: workspace_root.display().to_string(),
32                });
33            }
34            contents
35        };
36
37        let data =
38            std::fs::read_to_string(&contents_path).map_err(|e| WorkspaceError::ReadError {
39                path: contents_path.display().to_string(),
40                description: format!("{e}"),
41            })?;
42
43        let workspace_dir = contents_path
44            .parent()
45            .map(Path::to_path_buf)
46            .unwrap_or_else(|| PathBuf::from("."));
47
48        let mut project_paths = Vec::new();
49        for location in extract_locations(&data) {
50            if let Some(path) = resolve_location(&workspace_dir, &location) {
51                if path.extension().is_some_and(|ext| ext == "xcodeproj") {
52                    project_paths.push(path);
53                }
54            }
55        }
56
57        Ok(Self { project_paths })
58    }
59}
60
61fn extract_locations(data: &str) -> Vec<String> {
62    let mut locations = Vec::new();
63    let needle = "location=\"";
64    let mut start = 0;
65    while let Some(pos) = data[start..].find(needle) {
66        let idx = start + pos + needle.len();
67        if let Some(end) = data[idx..].find('"') {
68            locations.push(data[idx..idx + end].to_string());
69            start = idx + end + 1;
70        } else {
71            break;
72        }
73    }
74    locations
75}
76
77fn resolve_location(workspace_dir: &Path, location: &str) -> Option<PathBuf> {
78    if let Some(rest) = location.strip_prefix("group:") {
79        Some(workspace_dir.join(rest))
80    } else if let Some(rest) = location.strip_prefix("container:") {
81        Some(workspace_dir.join(rest))
82    } else if let Some(rest) = location.strip_prefix("absolute:") {
83        Some(PathBuf::from(rest))
84    } else if location.starts_with('/') {
85        Some(PathBuf::from(location))
86    } else {
87        Some(workspace_dir.join(location))
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use std::fs;
95    use tempfile::tempdir;
96
97    #[test]
98    fn parses_workspace_file_refs() {
99        let dir = tempdir().expect("tempdir");
100        let workspace_dir = dir.path().join("Demo.xcworkspace");
101        fs::create_dir_all(&workspace_dir).expect("workspace dir");
102        let contents = workspace_dir.join("contents.xcworkspacedata");
103        fs::write(
104            &contents,
105            r#"<?xml version="1.0" encoding="UTF-8"?>
106<Workspace version="1.0">
107   <FileRef location="group:Demo.xcodeproj"></FileRef>
108</Workspace>"#,
109        )
110        .expect("write contents");
111
112        let workspace = Xcworkspace::from_path(&workspace_dir).expect("parse workspace");
113        assert_eq!(workspace.project_paths.len(), 1);
114        assert!(workspace.project_paths[0].ends_with("Demo.xcodeproj"));
115    }
116}