1use crossterm::style::Color;
2
3pub 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
19pub struct HeartConfig {
21 pub message: Option<String>,
22 pub petite: bool,
23 pub color: String,
24}
25
26pub fn sanitize_input(input: &str) -> String {
28 input
29 .chars()
30 .filter(|&c| {
31 let code = c as u32;
34 (0x20..0x7F).contains(&code) || c == '\t' || c == '\n'
35 })
36 .collect()
37}
38
39pub 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
53pub 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
67pub 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
92pub 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 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 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 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 assert_eq!(validate_message("愛してる💜").unwrap(), "");
260 }
261 }
262 }
263
264 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 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 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}