Skip to main content

piper_client/control/
zeroing_token.rs

1//! 关节归零确认令牌
2//!
3//! **安全机制**:关节归零是危险操作,可能导致机械臂撞击限位或损坏设备。
4//! 因此引入确认令牌机制,强制用户明确确认此操作。
5//!
6//! # 三种构造方式
7//!
8//! 1. **`confirm_from_env()`**(推荐用于 CLI 应用)
9//!    - 从环境变量 `PIPER_ZEROING_CONFIRM` 读取确认
10//!    - 值必须是 "I_CONFIRM_ZEROING_IS_DANGEROUS"
11//!    - 推荐用于生产环境
12//!
13//! 2. **`unsafe fn new_unchecked()`**(供 GUI 应用使用)
14//!    - 不检查任何条件,直接创建令牌
15//!    - 用户必须在 UI 中已经确认
16//!    - 安全性由 GUI 应用层保证
17//!
18//! 3. **`confirm_for_test()`**(仅测试可用)
19//!    - 仅在 `cfg(test)` 下可用
20//!    - 用于单元测试和集成测试
21//!
22//! # 使用示例
23//!
24//! ## CLI 应用
25//!
26//! ```rust,no_run
27//! use piper_client::control::ZeroingConfirmToken;
28//!
29//! // 用户需要在命令行明确确认:
30//! // export PIPER_ZEROING_CONFIRM=I_CONFIRM_ZEROING_IS_DANGEROUS
31//! let token = ZeroingConfirmToken::confirm_from_env()?;
32//! # Ok::<(), Box<dyn std::error::Error>>(())
33//! ```
34//!
35//! ## GUI 应用
36//!
37//! ```rust,no_run
38//! use piper_client::control::ZeroingConfirmToken;
39//! use std::io;
40//!
41//! fn main() -> Result<(), Box<dyn std::error::Error>> {
42//!     // 显示确认对话框,用户点击"我确认"
43//!     if show_confirmation_dialog() {
44//!         // ⚠️ 用户已在 UI 中确认,这里使用 unsafe 跳过检查
45//!         let token = unsafe { ZeroingConfirmToken::new_unchecked() };
46//!     } else {
47//!         return Err(Box::new(io::Error::new(
48//!             io::ErrorKind::Other,
49//!             "User cancelled"
50//!         )) as Box<dyn std::error::Error>);
51//!     }
52//!     Ok(())
53//! }
54//! # fn show_confirmation_dialog() -> bool { true }
55//! ```
56//!
57//! ## 测试代码
58//!
59//! ```rust,ignore
60//! use piper_client::control::ZeroingConfirmToken;
61//!
62//! #[test]
63//! fn test_zeroing() {
64//!     let token = ZeroingConfirmToken::confirm_for_test();
65//!     // 执行归零操作...
66//! }
67//! ```
68
69use std::env;
70use std::io;
71
72/// 环境变量名称
73const ENV_VAR: &str = "PIPPER_ZEROING_CONFIRM";
74
75/// 环境变量期望值(必须完全匹配)
76const ENV_VALUE: &str = "I_CONFIRM_ZEROING_IS_DANGEROUS";
77
78/// 关节归零确认令牌(Zero-token 类型模式)
79///
80/// **安全机制**:此类型只能通过三种方式创建:
81/// 1. 从环境变量读取(`confirm_from_env()`)
82/// 2. unsafe 创建(`new_unchecked()`)
83/// 3. 测试创建(`confirm_for_test()`,仅测试可用)
84///
85/// 这确保了用户明确确认了归零操作的 danger。
86///
87/// # 设计说明
88///
89/// 这是一个 **Zero-cost 类型安全** 模式:
90/// - 类型大小:0 字节(ZST,零大小类型)
91/// - 运行时开销:0
92/// - 编译期检查:✅
93///
94/// # 示例
95///
96/// ```rust,no_run
97/// # use piper_client::control::ZeroingConfirmToken;
98/// // 从环境变量读取(推荐)
99/// let token = ZeroingConfirmToken::confirm_from_env()?;
100///
101/// // 或使用 unsafe(仅用于 GUI)
102/// let token = unsafe { ZeroingConfirmToken::new_unchecked() };
103/// # Ok::<(), Box<dyn std::error::Error>>(())
104/// ```
105#[derive(Debug, Clone, Copy)]
106pub struct ZeroingConfirmToken(());
107
108impl ZeroingConfirmToken {
109    /// 从环境变量确认(推荐用于 CLI 应用)
110    ///
111    /// **环境变量**:`PIPPER_ZEROING_CONFIRM`
112    /// **期望值**:`I_CONFIRM_ZEROING_IS_DANGEROUS`
113    ///
114    /// # 错误
115    ///
116    /// - 环境变量未设置
117    /// - 环境变量值不匹配
118    ///
119    /// # 示例
120    ///
121    /// ```rust,no_run
122    /// # use piper_client::control::ZeroingConfirmToken;
123    /// // 用户需要在命令行明确确认:
124    /// // export PIPER_ZEROING_CONFIRM=I_CONFIRM_ZEROING_IS_DANGEROUS
125    /// let token = ZeroingConfirmToken::confirm_from_env()?;
126    /// # Ok::<(), Box<dyn std::error::Error>>(())
127    /// ```
128    pub fn confirm_from_env() -> Result<Self, ZeroingTokenError> {
129        let value = env::var(ENV_VAR).map_err(|_| ZeroingTokenError::EnvNotSet {
130            var: ENV_VAR.to_string(),
131        })?;
132
133        if value == ENV_VALUE {
134            Ok(Self(()))
135        } else {
136            Err(ZeroingTokenError::EnvValueMismatch {
137                expected: ENV_VALUE.to_string(),
138                found: value,
139            })
140        }
141    }
142
143    /// 不安全创建(供 GUI 应用使用)
144    ///
145    /// **⚠️ 安全契约**:
146    ///
147    /// 调用此方法前,必须确保:
148    /// 1. 用户已在 UI 中明确确认归零操作的 danger
149    /// 2. 显示了清晰的警告信息
150    /// 3. 用户主动点击了"确认"按钮(或其他明确的确认动作)
151    ///
152    /// # Safety
153    ///
154    /// 调用者必须保证用户已经明确确认了归零操作的 danger。
155    /// 此函数绕过了环境变量检查,因此调用者有责任确保用户同意。
156    ///
157    /// # 示例
158    ///
159    /// ```rust,no_run
160    /// # use piper_client::control::ZeroingConfirmToken;
161    /// # use std::io;
162    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
163    /// // 显示确认对话框
164    /// if show_confirmation_dialog() {
165    ///     // ⚠️ 用户已确认,使用 unsafe 跳过检查
166    ///     let token = unsafe { ZeroingConfirmToken::new_unchecked() };
167    /// } else {
168    ///     return Err(Box::new(io::Error::new(
169    ///         io::ErrorKind::Other,
170    ///         "User cancelled"
171    ///     )) as Box<dyn std::error::Error>);
172    /// }
173    /// # Ok(())
174    /// # }
175    /// # fn show_confirmation_dialog() -> bool { true }
176    /// ```
177    pub unsafe fn new_unchecked() -> Self {
178        Self(())
179    }
180
181    /// 测试用创建(仅测试可用)
182    ///
183    /// **⚠️ 注意**:此方法仅在 `cfg(test)` 下可用。
184    ///
185    /// # 示例
186    ///
187    /// ```rust,ignore
188    /// # use piper_client::control::ZeroingConfirmToken;
189    /// #[test]
190    /// fn test_zeroing() {
191    ///     let token = ZeroingConfirmToken::confirm_for_test();
192    ///     // 执行归零操作...
193    /// }
194    /// ```
195    #[cfg(test)]
196    pub fn confirm_for_test() -> Self {
197        Self(())
198    }
199}
200
201/// ZeroingToken 错误
202#[derive(Debug, thiserror::Error)]
203pub enum ZeroingTokenError {
204    /// 环境变量未设置
205    #[error(
206        "Environment variable not set. \
207            Please export {var}='I_CONFIRM_ZEROING_IS_DANGEROUS' to confirm you understand the risks."
208    )]
209    EnvNotSet { var: String },
210
211    /// 环境变量值不匹配
212    #[error(
213        "Environment variable has incorrect value. \
214            Expected: 'I_CONFIRM_ZEROING_IS_DANGEROUS', Found: '{found}'"
215    )]
216    EnvValueMismatch { expected: String, found: String },
217
218    /// IO 错误(用于兼容)
219    #[error("IO error: {0}")]
220    Io(#[from] io::Error),
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226    use serial_test::serial;
227    use std::env;
228
229    #[test]
230    #[serial]
231    fn test_confirm_from_env_success() {
232        unsafe {
233            env::remove_var(ENV_VAR); // 先清理,避免测试间干扰
234            env::set_var(ENV_VAR, ENV_VALUE);
235        }
236        let token = ZeroingConfirmToken::confirm_from_env();
237        assert!(token.is_ok());
238        unsafe {
239            env::remove_var(ENV_VAR);
240        }
241    }
242
243    #[test]
244    #[serial]
245    fn test_confirm_from_env_not_set() {
246        unsafe {
247            env::remove_var(ENV_VAR);
248        }
249        let token = ZeroingConfirmToken::confirm_from_env();
250        assert!(matches!(token, Err(ZeroingTokenError::EnvNotSet { .. })));
251    }
252
253    #[test]
254    #[serial]
255    fn test_confirm_from_env_wrong_value() {
256        unsafe {
257            env::remove_var(ENV_VAR); // 先清理,避免测试间干扰
258            env::set_var(ENV_VAR, "wrong_value");
259        }
260        let token = ZeroingConfirmToken::confirm_from_env();
261        assert!(matches!(
262            token,
263            Err(ZeroingTokenError::EnvValueMismatch { .. })
264        ));
265        unsafe {
266            env::remove_var(ENV_VAR);
267        }
268    }
269
270    #[test]
271    fn test_confirm_for_test() {
272        let token = ZeroingConfirmToken::confirm_for_test();
273        // Zero-size type,无法直接比较,但至少可以创建
274        let _ = token;
275    }
276
277    #[test]
278    fn test_new_unchecked() {
279        // unsafe 块内的代码可以编译
280        let token = unsafe { ZeroingConfirmToken::new_unchecked() };
281        let _ = token;
282    }
283
284    #[test]
285    fn test_token_is_zero_sized() {
286        assert_eq!(std::mem::size_of::<ZeroingConfirmToken>(), 0);
287    }
288
289    #[test]
290    fn test_token_is_copy() {
291        let token1 = unsafe { ZeroingConfirmToken::new_unchecked() };
292        let token2 = token1; // Copy,token1 仍然有效
293        let _ = (token1, token2);
294    }
295}