teams_cli/desktop/
browser_setup.rs1use super::browser::{data_directory, prepare_directory, validate_directory};
3use crate::{
4 config::{PendingBrowserSetup, Profile},
5 error::AppError,
6};
7use std::{
8 io::{Read, Write},
9 path::Path,
10};
11
12const MARKER: &str = "pending-setup.json";
13
14fn read(path: &Path, port: u16) -> Result<PendingBrowserSetup, AppError> {
15 validate_directory(path)?;
16 let file = std::fs::File::open(path.join(MARKER))?;
17 if file.metadata()?.len() > 4096 {
18 return Err(AppError::Desktop(
19 "The pending browser setup record is too large; it was preserved for recovery.".into(),
20 ));
21 }
22 let mut bytes = Vec::new();
23 file.take(4097).read_to_end(&mut bytes)?;
24 let record: PendingBrowserSetup = serde_json::from_slice(&bytes).map_err(|_| {
25 AppError::Desktop(
26 "The pending browser setup record is invalid; it was preserved for recovery.".into(),
27 )
28 })?;
29 if record.port != port
30 || port < 1024
31 || !record.client.is_browser()
32 || record.name.is_empty()
33 || record.name.chars().any(char::is_control)
34 {
35 return Err(AppError::Desktop("The pending browser setup record does not match its browser; it was preserved for recovery.".into()));
36 }
37 record.client.validate_account(record.account)?;
38 Ok(record)
39}
40
41fn list_in(base: &Path) -> Result<Vec<PendingBrowserSetup>, AppError> {
42 if !base.try_exists()? {
43 return Ok(Vec::new());
44 }
45 validate_directory(base)?;
46 let mut records = Vec::new();
47 for entry in std::fs::read_dir(base)? {
48 let entry = entry?;
49 let Some(port) = entry
50 .file_name()
51 .to_str()
52 .and_then(|s| s.parse::<u16>().ok())
53 else {
54 continue;
55 };
56 if entry.path().join(MARKER).try_exists()? {
57 records.push(read(&entry.path(), port).map_err(|error| {
58 error.with_hint(&format!(
59 "Check {}. To abandon only this unfinished setup, remove that record file; keep its browser directory to preserve sign-in data.",
60 entry.path().join(MARKER).display()
61 ))
62 })?);
63 }
64 }
65 records.sort_by_key(|record| record.port);
66 Ok(records)
67}
68
69pub fn list() -> Result<Vec<PendingBrowserSetup>, AppError> {
70 list_in(&data_directory()?)
71}
72
73pub fn begin(name: &str, profile: &Profile) -> Result<(), AppError> {
74 if name.is_empty() || name.len() > 256 || name.chars().any(char::is_control) {
75 return Err(AppError::InvalidInput(
76 "A browser setup profile name must contain 1–256 bytes without control characters."
77 .into(),
78 ));
79 }
80 let dir = data_directory()?.join(profile.desktop_port.to_string());
81 prepare_directory(&dir)?;
82 let record = PendingBrowserSetup {
83 name: name.into(),
84 port: profile.desktop_port,
85 account: profile.desktop_account,
86 client: profile.desktop_client,
87 };
88 if dir.join(MARKER).try_exists()? {
89 if read(&dir, record.port)? == record {
90 return Ok(());
91 }
92 return Err(AppError::Desktop("This browser has another unfinished setup; select its original connection or use another browser port.".into()));
93 }
94 let mut file = tempfile::NamedTempFile::new_in(&dir)?;
95 file.write_all(&serde_json::to_vec(&record).map_err(|e| AppError::Unexpected(e.to_string()))?)?;
96 file.as_file().sync_all()?;
97 file.persist_noclobber(dir.join(MARKER))
98 .map_err(|e| e.error)?;
99 Ok(())
100}
101
102pub fn finish(port: u16) -> Result<(), AppError> {
103 let dir = data_directory()?.join(port.to_string());
104 validate_directory(&dir)?;
105 match std::fs::remove_file(dir.join(MARKER)) {
106 Ok(()) => Ok(()),
107 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
108 Err(error) => Err(error.into()),
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 #[test]
116 fn pending_records_are_bounded_and_cannot_cross_a_port_or_symlink() {
117 use std::os::unix::fs::{PermissionsExt, symlink};
118 let base = tempfile::tempdir().unwrap();
119 std::fs::set_permissions(base.path(), std::fs::Permissions::from_mode(0o700)).unwrap();
120 let dir = base.path().join("19300");
121 prepare_directory(&dir).unwrap();
122 let record = PendingBrowserSetup {
123 name: "work".into(),
124 port: 19300,
125 account: crate::config::DesktopAccount::WorkSchool,
126 client: crate::config::DesktopClientKind::Browser,
127 };
128 std::fs::write(dir.join(MARKER), serde_json::to_vec(&record).unwrap()).unwrap();
129 assert_eq!(list_in(base.path()).unwrap(), vec![record]);
130 assert!(read(&dir, 19301).is_err());
131 symlink(&dir, base.path().join("19301")).unwrap();
132 assert!(list_in(base.path()).is_err());
133 std::fs::remove_file(base.path().join("19301")).unwrap();
134 std::fs::write(dir.join(MARKER), vec![b' '; 4097]).unwrap();
135 assert!(list_in(base.path()).is_err());
136 assert_eq!(std::fs::metadata(dir.join(MARKER)).unwrap().len(), 4097);
137 }
138}