verifyos_cli/parsers/
xcworkspace_parser.rs1use 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}