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}