1use std::fmt::Write;
23
24pub fn apply(hint: &str, data: &[u8]) -> String {
63 if hint.is_empty() || data.is_empty() {
64 return hex_encode(data);
65 }
66
67 let hint = hint.as_bytes();
68 let mut result = String::with_capacity(data.len() * 4);
69 let mut hint_pos = 0;
70 let mut data_pos = 0;
71 let mut last_spec_start = 0;
72
73 while data_pos < data.len() {
74 if hint_pos >= hint.len() {
76 hint_pos = last_spec_start;
77 }
78
79 let spec_start = hint_pos;
80
81 let star_prefix = if hint_pos < hint.len() && hint[hint_pos] == b'*' {
83 hint_pos += 1;
84 true
85 } else {
86 false
87 };
88
89 if hint_pos >= hint.len() || !is_digit(hint[hint_pos]) {
91 return hex_encode(data);
92 }
93
94 let mut take = 0usize;
95 while hint_pos < hint.len() && is_digit(hint[hint_pos]) {
96 take = take * 10 + (hint[hint_pos] - b'0') as usize;
97 hint_pos += 1;
98 }
99
100 if take == 0 {
101 return hex_encode(data);
102 }
103
104 if hint_pos >= hint.len() {
106 return hex_encode(data);
107 }
108
109 let fmt_char = hint[hint_pos];
110 if !matches!(fmt_char, b'd' | b'x' | b'o' | b'a' | b't') {
111 return hex_encode(data);
112 }
113 hint_pos += 1;
114
115 let (sep, has_sep) =
117 if hint_pos < hint.len() && !is_digit(hint[hint_pos]) && hint[hint_pos] != b'*' {
118 let s = hint[hint_pos];
119 hint_pos += 1;
120 (s, true)
121 } else {
122 (0, false)
123 };
124
125 let (term, has_term) = if star_prefix
127 && hint_pos < hint.len()
128 && !is_digit(hint[hint_pos])
129 && hint[hint_pos] != b'*'
130 {
131 let t = hint[hint_pos];
132 hint_pos += 1;
133 (t, true)
134 } else {
135 (0, false)
136 };
137
138 last_spec_start = spec_start;
140
141 let repeat_count = if star_prefix && data_pos < data.len() {
143 let count = data[data_pos] as usize;
144 data_pos += 1;
145 count
146 } else {
147 1
148 };
149
150 for r in 0..repeat_count {
151 if data_pos >= data.len() {
152 break;
153 }
154
155 let end = (data_pos + take).min(data.len());
156 let chunk = &data[data_pos..end];
157
158 match fmt_char {
160 b'd' => {
161 let val = chunk.iter().fold(0u64, |acc, &b| (acc << 8) | u64::from(b));
163 let _ = write!(result, "{}", val);
164 }
165 b'x' => {
166 write_hex(&mut result, chunk);
168 }
169 b'o' => {
170 let val = chunk.iter().fold(0u64, |acc, &b| (acc << 8) | u64::from(b));
172 let _ = write!(result, "{:o}", val);
173 }
174 b'a' | b't' => {
175 for &b in chunk {
177 result.push(b as char);
178 }
179 }
180 _ => unreachable!(),
181 }
182 data_pos = end;
183
184 let more_data = data_pos < data.len();
186 if has_sep && more_data {
187 if has_term && r == repeat_count - 1 {
189 } else {
191 result.push(sep as char);
192 }
193 }
194 }
195
196 if has_term && data_pos < data.len() {
198 result.push(term as char);
199 }
200 }
201
202 result
203}
204
205fn hex_encode(data: &[u8]) -> String {
207 let mut out = String::with_capacity(data.len() * 2);
208 write_hex(&mut out, data);
209 out
210}
211
212fn write_hex(out: &mut String, data: &[u8]) {
214 const HEX_TABLE: &[u8; 16] = b"0123456789abcdef";
215 for &b in data {
216 out.push(HEX_TABLE[(b >> 4) as usize] as char);
217 out.push(HEX_TABLE[(b & 0x0f) as usize] as char);
218 }
219}
220
221#[inline]
222fn is_digit(c: u8) -> bool {
223 c.is_ascii_digit()
224}
225
226pub fn apply_integer(hint: &str, value: i32) -> Option<String> {
256 match hint {
257 "x" => Some(format!("{:x}", value)),
258 "o" => Some(format!("{:o}", value)),
259 "b" => Some(format!("{:b}", value)),
260 "d" => Some(format!("{}", value)),
261 hint if hint.starts_with("d-") => {
262 let places: usize = hint[2..].parse().ok()?;
263 if places == 0 {
264 return Some(format!("{}", value));
265 }
266 Some(format_with_decimal_point(value, places))
267 }
268 _ => None,
269 }
270}
271
272fn format_with_decimal_point(value: i32, places: usize) -> String {
276 let is_negative = value < 0;
277 let abs_value = value.unsigned_abs();
278 let abs_str = abs_value.to_string();
279
280 let result = if abs_str.len() <= places {
281 let zeros_needed = places - abs_str.len();
284 format!("0.{}{}", "0".repeat(zeros_needed), abs_str)
285 } else {
286 let split_point = abs_str.len() - places;
289 let (integer_part, decimal_part) = abs_str.split_at(split_point);
290 format!("{}.{}", integer_part, decimal_part)
291 };
292
293 if is_negative {
294 format!("-{}", result)
295 } else {
296 result
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
309 fn empty_hint_returns_hex() {
310 assert_eq!(apply("", &[0x01, 0x02, 0x03]), "010203");
311 }
312
313 #[test]
314 fn empty_data_returns_empty() {
315 assert_eq!(apply("1d", &[]), "");
316 }
317
318 #[test]
319 fn ipv4_address() {
320 assert_eq!(apply("1d.1d.1d.1d", &[192, 168, 1, 1]), "192.168.1.1");
321 }
322
323 #[test]
324 fn ipv4_with_zone_id() {
325 assert_eq!(
326 apply("1d.1d.1d.1d%4d", &[192, 168, 1, 1, 0, 0, 0, 3]),
327 "192.168.1.1%3"
328 );
329 }
330
331 #[test]
332 fn mac_address() {
333 assert_eq!(
334 apply("1x:", &[0x00, 0x1a, 0x2b, 0x3c, 0x4d, 0x5e]),
335 "00:1a:2b:3c:4d:5e"
336 );
337 }
338
339 #[test]
340 fn ipv6_address() {
341 let data = [
342 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
343 0x00, 0x01,
344 ];
345 assert_eq!(
346 apply("2x:2x:2x:2x:2x:2x:2x:2x", &data),
347 "2001:0db8:0000:0000:0000:0000:0000:0001"
348 );
349 }
350
351 #[test]
352 fn ipv6_with_zone_id() {
353 let data = [
354 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
355 0x00, 0x01, 0x00, 0x00, 0x00, 0x05,
356 ];
357 assert_eq!(
358 apply("2x:2x:2x:2x:2x:2x:2x:2x%4d", &data),
359 "fe80:0000:0000:0000:0000:0000:0000:0001%5"
360 );
361 }
362
363 #[test]
364 fn display_string() {
365 assert_eq!(apply("255a", b"Hello, World!"), "Hello, World!");
366 }
367
368 #[test]
369 fn simple_decimal() {
370 assert_eq!(apply("1d", &[42]), "42");
371 }
372
373 #[test]
374 fn multi_byte_decimal() {
375 assert_eq!(apply("4d", &[0x00, 0x01, 0x00, 0x00]), "65536");
376 }
377
378 #[test]
379 fn octal_format() {
380 assert_eq!(apply("1o", &[8]), "10");
381 }
382
383 #[test]
384 fn hex_with_dash_separator() {
385 assert_eq!(apply("1x-", &[0xaa, 0xbb, 0xcc]), "aa-bb-cc");
386 }
387
388 #[test]
389 fn star_prefix_repeat() {
390 assert_eq!(apply("*1x:", &[3, 0xaa, 0xbb, 0xcc]), "aa:bb:cc");
391 }
392
393 #[test]
394 fn star_prefix_with_terminator() {
395 assert_eq!(apply("*1d./1d", &[3, 10, 20, 30, 40]), "10.20.30/40");
396 }
397
398 #[test]
399 fn trailing_separator_suppressed() {
400 assert_eq!(apply("1d.", &[1, 2, 3]), "1.2.3");
401 }
402
403 #[test]
404 fn date_and_time() {
405 assert_eq!(
406 apply("2d-1d-1d,1d:1d:1d.1d", &[0x07, 0xE6, 8, 15, 8, 1, 15, 0]),
407 "2022-8-15,8:1:15.0"
408 );
409 }
410
411 #[test]
412 fn data_shorter_than_spec() {
413 assert_eq!(apply("1d.1d.1d.1d", &[10, 20]), "10.20");
414 }
415
416 #[test]
417 fn utf8_format() {
418 assert_eq!(apply("10t", b"hello"), "hello");
419 }
420
421 #[test]
422 fn uuid_format() {
423 let data = [
424 0x12, 0x34, 0x56, 0x78, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x00, 0x11, 0x22, 0x33,
425 0x44, 0x55,
426 ];
427 assert_eq!(
428 apply("4x-2x-2x-1x1x-6x", &data),
429 "12345678-abcd-ef01-2345-001122334455"
430 );
431 }
432
433 #[test]
434 fn ipv4_with_prefix() {
435 assert_eq!(apply("1d.1d.1d.1d/1d", &[10, 0, 0, 0, 24]), "10.0.0.0/24");
436 }
437
438 #[test]
439 fn two_digit_take_value() {
440 assert_eq!(
441 apply("10d", &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]),
442 "1"
443 );
444 }
445
446 #[test]
447 fn zero_padded_hex_output() {
448 assert_eq!(apply("1x", &[0x0f]), "0f");
449 }
450
451 #[test]
452 fn single_byte_trailing_separator_suppressed() {
453 assert_eq!(apply("1d.", &[42]), "42");
454 }
455
456 #[test]
461 fn invalid_format_character() {
462 assert_eq!(apply("1z", &[1, 2, 3]), "010203");
463 }
464
465 #[test]
466 fn missing_format_character() {
467 assert_eq!(apply("1", &[1, 2, 3]), "010203");
468 }
469
470 #[test]
471 fn missing_take_value() {
472 assert_eq!(apply("d", &[1, 2, 3]), "010203");
473 }
474
475 #[test]
476 fn zero_take_value() {
477 assert_eq!(apply("0d", &[1, 2, 3]), "010203");
478 }
479
480 #[test]
485 fn single_spec_repeats_for_all_data() {
486 assert_eq!(apply("1d.", &[1, 2, 3, 4, 5]), "1.2.3.4.5");
487 }
488
489 #[test]
490 fn last_spec_repeats_after_fixed_prefix() {
491 assert_eq!(apply("1d-1d.", &[1, 2, 3, 4, 5, 6]), "1-2.3.4.5.6");
492 }
493
494 #[test]
495 fn hex_implicit_repetition() {
496 assert_eq!(apply("1x:", &[0xaa, 0xbb, 0xcc, 0xdd]), "aa:bb:cc:dd");
497 }
498
499 #[test]
504 fn integer_hint_decimal() {
505 assert_eq!(apply_integer("d", 1234), Some("1234".to_string()));
506 assert_eq!(apply_integer("d", -42), Some("-42".to_string()));
507 assert_eq!(apply_integer("d", 0), Some("0".to_string()));
508 }
509
510 #[test]
511 fn integer_hint_hex() {
512 assert_eq!(apply_integer("x", 255), Some("ff".to_string()));
513 assert_eq!(apply_integer("x", 0), Some("0".to_string()));
514 assert_eq!(apply_integer("x", 16), Some("10".to_string()));
515 assert_eq!(apply_integer("x", -1), Some("ffffffff".to_string()));
517 }
518
519 #[test]
520 fn integer_hint_octal() {
521 assert_eq!(apply_integer("o", 8), Some("10".to_string()));
522 assert_eq!(apply_integer("o", 64), Some("100".to_string()));
523 assert_eq!(apply_integer("o", 0), Some("0".to_string()));
524 }
525
526 #[test]
527 fn integer_hint_binary() {
528 assert_eq!(apply_integer("b", 5), Some("101".to_string()));
529 assert_eq!(apply_integer("b", 255), Some("11111111".to_string()));
530 assert_eq!(apply_integer("b", 0), Some("0".to_string()));
531 }
532
533 #[test]
534 fn integer_hint_decimal_places() {
535 assert_eq!(apply_integer("d-2", 1234), Some("12.34".to_string()));
537 assert_eq!(apply_integer("d-1", 255), Some("25.5".to_string()));
538 assert_eq!(apply_integer("d-3", 12500), Some("12.500".to_string()));
539
540 assert_eq!(apply_integer("d-2", 5), Some("0.05".to_string()));
542 assert_eq!(apply_integer("d-2", 50), Some("0.50".to_string()));
543 assert_eq!(apply_integer("d-3", 5), Some("0.005".to_string()));
544
545 assert_eq!(apply_integer("d-2", 0), Some("0.00".to_string()));
547
548 assert_eq!(apply_integer("d-2", -500), Some("-5.00".to_string()));
550 assert_eq!(apply_integer("d-2", -5), Some("-0.05".to_string()));
551 assert_eq!(apply_integer("d-1", -42), Some("-4.2".to_string()));
552
553 assert_eq!(apply_integer("d-0", 1234), Some("1234".to_string()));
555 }
556
557 #[test]
558 fn integer_hint_invalid() {
559 assert_eq!(apply_integer("", 42), None);
560 assert_eq!(apply_integer("z", 42), None);
561 assert_eq!(apply_integer("d-abc", 42), None);
562 assert_eq!(apply_integer("1d", 42), None); }
564}