Skip to main content

tolove_ru/
lib.rs

1use crossterm::style::Color;
2
3// ハートの描画領域
4pub const HEART_SIZE_L: i32 = 20;
5pub const HEART_SIZE_S: i32 = 10;
6
7pub const ABOUT_MESSAGE: &str = "
8┌---------------------------------------------------------------------------┐
9|   vvvvvv  vvvvvvv      A lovely terminal heart animation.                 |
10| vvvvvvvvvvvvvvvvvv                                                        |
11| vvvvvvvvvvvvvvvvvvv    Watch the heart float up...                        |
12| vvvvvvvvvvvvvvvvvv     Add your message inside...                         |
13|   vvvvvvvvvvvvvv       And share the love!                                |
14|     vvvvvvvvvv                                                            |
15|       vvvvvv           Type 'love --help' for more details                |
16|         vv                                                                |
17└---------------------------------------------------------------------------┘";
18
19/// CLIフレームワーク非依存のハート設定
20pub struct HeartConfig {
21    pub message: Option<String>,
22    pub petite: bool,
23    pub color: String,
24}
25
26/// 入力をサニタイズし、制御文字やエスケープシーケンスを除去する
27pub fn sanitize_input(input: &str) -> String {
28    input
29        .chars()
30        .filter(|&c| {
31            // 印字可能文字、スペース、タブ、改行のみ許可
32            // 制御文字 (0x00-0x1F, 0x7F-0x9F) を除去
33            let code = c as u32;
34            (0x20..0x7F).contains(&code) || c == '\t' || c == '\n'
35        })
36        .collect()
37}
38
39/// メッセージ入力のバリデーションとサニタイズ
40pub fn validate_message(s: &str) -> Result<String, String> {
41    const MAX_MESSAGE_LENGTH: usize = 100;
42
43    if s.len() > MAX_MESSAGE_LENGTH {
44        return Err(format!(
45            "Message too long (max {} characters)",
46            MAX_MESSAGE_LENGTH
47        ));
48    }
49
50    Ok(sanitize_input(s))
51}
52
53/// 色名文字列を対応するColorに変換する
54pub fn parse_color(color_str: &str) -> Color {
55    match color_str {
56        "red" => Color::Red,
57        "green" => Color::Green,
58        "blue" => Color::Blue,
59        "yellow" => Color::Yellow,
60        "magenta" => Color::Magenta,
61        "cyan" => Color::Cyan,
62        "white" => Color::White,
63        _ => Color::White,
64    }
65}
66
67/// 座標がハート形状の内部にあるかを判定する
68pub fn is_in_love(x: i32, y: i32, config: &HeartConfig) -> bool {
69    let (heart_size, _) = heart_sizes(config);
70
71    let width = 2.2;
72    let height = 3.0;
73    let heart_coefficient = 0.7;
74
75    let check_x = ((x as f64 / heart_size as f64) - 0.5) * width;
76    let check_y = (((heart_size - y) as f64 / heart_size as f64) - 0.4) * height;
77
78    let top_y: f64;
79    let bottom_y: f64;
80
81    if check_x >= 0.0 {
82        top_y = (1.0 - (check_x * check_x)).sqrt() + (heart_coefficient * check_x.sqrt());
83        bottom_y = -(1.0 - (check_x * check_x)).sqrt() + (heart_coefficient * check_x.sqrt());
84    } else {
85        top_y = (1.0 - (check_x * check_x)).sqrt() + (heart_coefficient * (-check_x).sqrt());
86        bottom_y = -(1.0 - (check_x * check_x)).sqrt() + (heart_coefficient * (-check_x).sqrt());
87    }
88
89    (bottom_y <= check_y) && (check_y <= top_y)
90}
91
92/// ハートのサイズを返す (幅, 半幅)
93pub fn heart_sizes(config: &HeartConfig) -> (i32, i32) {
94    if config.petite {
95        return (HEART_SIZE_S, HEART_SIZE_S / 2);
96    };
97
98    (HEART_SIZE_L, HEART_SIZE_L / 2)
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use crossterm::style::Color;
105    use rstest::rstest;
106
107    // テスト用のヘルパー: デフォルトのHeartConfigを生成
108    fn default_config() -> HeartConfig {
109        HeartConfig {
110            message: None,
111            petite: false,
112            color: "white".to_string(),
113        }
114    }
115
116    fn petite_config() -> HeartConfig {
117        HeartConfig {
118            message: None,
119            petite: true,
120            color: "white".to_string(),
121        }
122    }
123
124    // ================================================================
125    // sanitize_input: 入力サニタイズの仕様
126    // ================================================================
127
128    mod describe_sanitize_input {
129        use super::*;
130
131        mod 印字可能なascii文字を受け取った場合 {
132            use super::*;
133
134            #[test]
135            fn そのまま返す() {
136                assert_eq!(sanitize_input("Hello World"), "Hello World");
137            }
138        }
139
140        mod 制御文字を受け取った場合 {
141            use super::*;
142
143            #[rstest]
144            #[case::ansi_esc("\x1b[31mRed\x1b[0m", "[31mRed[0m")]
145            #[case::ベル文字("\x07Bell", "Bell")]
146            #[case::ヌルバイト("Hello\x00World", "HelloWorld")]
147            #[case::del文字("Text\x7FMore", "TextMore")]
148            #[case::c1制御コード("Test\u{009B}More", "TestMore")]
149            fn 除去する(#[case] input: &str, #[case] expected: &str) {
150                assert_eq!(sanitize_input(input), expected);
151            }
152        }
153
154        mod ターミナルインジェクション攻撃を受けた場合 {
155            use super::*;
156
157            #[rstest]
158            #[case::タイトルインジェクション("\x1b]0;Evil\x07", "]0;Evil")]
159            #[case::画面クリアインジェクション("\x1b[2J\x1b[H", "[2J[H")]
160            #[case::複合攻撃("\x1b[31m\x00\x07Evil\x1b[0m", "[31mEvil[0m")]
161            fn 制御文字のみ除去して無害化する(
162                #[case] input: &str,
163                #[case] expected: &str,
164            ) {
165                assert_eq!(sanitize_input(input), expected);
166            }
167        }
168
169        mod タブや改行を受け取った場合 {
170            use super::*;
171
172            #[test]
173            fn 保持する() {
174                assert_eq!(sanitize_input("Line1\tTab\nLine2"), "Line1\tTab\nLine2");
175            }
176        }
177
178        mod unicodeや絵文字を受け取った場合 {
179            use super::*;
180
181            #[test]
182            fn ascii範囲外のため除去する() {
183                assert_eq!(sanitize_input("❤️💜"), "");
184            }
185        }
186
187        mod 空文字列を受け取った場合 {
188            use super::*;
189
190            #[test]
191            fn 空文字列を返す() {
192                assert_eq!(sanitize_input(""), "");
193            }
194        }
195    }
196
197    // ================================================================
198    // validate_message: メッセージバリデーションの仕様
199    // ================================================================
200
201    mod describe_validate_message {
202        use super::*;
203
204        mod 正常なメッセージの場合 {
205            use super::*;
206
207            #[test]
208            fn サニタイズ済みの文字列を返す() {
209                let result = validate_message("I love you");
210                assert_eq!(result.unwrap(), "I love you");
211            }
212        }
213
214        mod メッセージの長さに関する境界値 {
215            use super::*;
216
217            #[test]
218            fn 最大長100文字は受け付ける() {
219                let input = "a".repeat(100);
220                assert!(validate_message(&input).is_ok());
221            }
222
223            #[rstest]
224            #[case::境界値超過(101)]
225            #[case::dos攻撃(1000)]
226            fn 最大長を超えるとエラーを返す(#[case] length: usize) {
227                let input = "a".repeat(length);
228                let result = validate_message(&input);
229                assert!(result.is_err());
230                assert!(result.unwrap_err().contains("Message too long"));
231            }
232        }
233
234        mod エスケープシーケンスを含む場合 {
235            use super::*;
236
237            #[test]
238            fn サニタイズして受け付ける() {
239                let result = validate_message("Hello\x1b[31mWorld");
240                assert_eq!(result.unwrap(), "Hello[31mWorld");
241            }
242        }
243
244        mod 空メッセージの場合 {
245            use super::*;
246
247            #[test]
248            fn 空文字列として受け付ける() {
249                assert_eq!(validate_message("").unwrap(), "");
250            }
251        }
252
253        mod unicodeメッセージの場合 {
254            use super::*;
255
256            #[test]
257            fn ascii範囲外の文字を除去して受け付ける() {
258                // セキュリティのため、ASCII範囲外の文字は除去される
259                assert_eq!(validate_message("愛してる💜").unwrap(), "");
260            }
261        }
262    }
263
264    // ================================================================
265    // parse_color: 色パースの仕様
266    // ================================================================
267
268    mod describe_parse_color {
269        use super::*;
270
271        mod 有効な色名の場合 {
272            use super::*;
273
274            #[rstest]
275            #[case::赤("red", Color::Red)]
276            #[case::緑("green", Color::Green)]
277            #[case::青("blue", Color::Blue)]
278            #[case::黄("yellow", Color::Yellow)]
279            #[case::マゼンタ("magenta", Color::Magenta)]
280            #[case::シアン("cyan", Color::Cyan)]
281            #[case::白("white", Color::White)]
282            fn 対応するcolorを返す(#[case] input: &str, #[case] expected: Color) {
283                assert_eq!(parse_color(input), expected);
284            }
285        }
286
287        mod 無効な色名の場合 {
288            use super::*;
289
290            #[rstest]
291            #[case::不明な文字列("invalid")]
292            #[case::空文字列("")]
293            #[case::大文字("RED")]
294            fn デフォルトの白色を返す(#[case] input: &str) {
295                assert_eq!(parse_color(input), Color::White);
296            }
297        }
298    }
299
300    // ================================================================
301    // heart_sizes: ハートサイズの仕様
302    // ================================================================
303
304    mod describe_heart_sizes {
305        use super::*;
306
307        mod 通常モードの場合 {
308            use super::*;
309
310            #[test]
311            fn 幅20_半幅10を返す() {
312                let (width, half) = heart_sizes(&default_config());
313                assert_eq!(width, 20);
314                assert_eq!(half, 10);
315            }
316        }
317
318        mod petiteモードの場合 {
319            use super::*;
320
321            #[test]
322            fn 幅10_半幅5を返す() {
323                let (width, half) = heart_sizes(&petite_config());
324                assert_eq!(width, 10);
325                assert_eq!(half, 5);
326            }
327        }
328    }
329
330    // ================================================================
331    // is_in_love: ハート形状判定の仕様
332    // ================================================================
333
334    mod describe_is_in_love {
335        use super::*;
336
337        mod ハート内部の座標の場合 {
338            use super::*;
339
340            #[test]
341            fn 通常サイズの中心はtrueを返す() {
342                assert!(is_in_love(10, 10, &default_config()));
343            }
344
345            #[test]
346            fn petiteサイズの中心はtrueを返す() {
347                assert!(is_in_love(5, 5, &petite_config()));
348            }
349        }
350
351        mod ハート外部の座標の場合 {
352            use super::*;
353
354            #[rstest]
355            #[case::左外(-5, 10)]
356            #[case::右外(25, 10)]
357            #[case::上外(10, 25)]
358            #[case::下外(10, -5)]
359            #[case::左上角(0, 0)]
360            #[case::右下角(20, 20)]
361            fn falseを返す(#[case] x: i32, #[case] y: i32) {
362                assert!(!is_in_love(x, y, &default_config()));
363            }
364        }
365
366        mod ハート境界付近の座標の場合 {
367            use super::*;
368
369            #[rstest]
370            #[case::左上寄り(5, 15)]
371            #[case::右上寄り(15, 15)]
372            #[case::下部(10, 5)]
373            fn 境界の外側はfalseを返す(#[case] x: i32, #[case] y: i32) {
374                assert!(!is_in_love(x, y, &default_config()));
375            }
376        }
377    }
378}