Skip to main content

visual_hashing/
randomart.rs

1// SPDX-FileCopyrightText: 2026 Blackcat Informatics® Inc. <paudley@blackcatinformatics.ca>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! OpenSSH-style "Drunken Bishop" ASCII-art fingerprint.
5//!
6//! A bishop starts in the centre of a 17Ă—9 grid and makes four moves per input
7//! byte (two bits each), incrementing a visit count on every square it lands on.
8//! The counts are rendered through a character ramp; the start and end squares
9//! are marked `S` and `E`. This is a byte-for-byte port of the Python reference.
10
11const WIDTH: usize = 17;
12const HEIGHT: usize = 9;
13
14/// Character ramp indexed by visit count. The first slot (count 0) is blank; the
15/// last is reserved for the end square. Matches OpenSSH's `augmentation_string`.
16const VALUES: &[u8] = b" .o+=*BOX@%&#/^";
17
18/// Render an OpenSSH-style randomart fingerprint of `data`.
19///
20/// `label` annotates the header (e.g. `"ED25519 256"`); pass `""` for none. The
21/// art is a deterministic function of `data`; `label` only affects the header.
22pub fn randomart(data: &[u8], label: &str) -> String {
23    let start_x = WIDTH / 2;
24    let start_y = HEIGHT / 2;
25    let mut grid = [[0u32; WIDTH]; HEIGHT];
26    let (mut x, mut y) = (start_x, start_y);
27
28    for &byte in data {
29        for shift in [0u32, 2, 4, 6] {
30            match (byte >> shift) & 0x3 {
31                0 => {
32                    y = y.saturating_sub(1);
33                    x = x.saturating_sub(1);
34                }
35                1 => {
36                    y = y.saturating_sub(1);
37                    x = (x + 1).min(WIDTH - 1);
38                }
39                2 => {
40                    y = (y + 1).min(HEIGHT - 1);
41                    x = x.saturating_sub(1);
42                }
43                _ => {
44                    y = (y + 1).min(HEIGHT - 1);
45                    x = (x + 1).min(WIDTH - 1);
46                }
47            }
48            grid[y][x] += 1;
49        }
50    }
51
52    let (end_x, end_y) = (x, y);
53    grid[start_y][start_x] = 0;
54    grid[end_y][end_x] = (VALUES.len() - 1) as u32;
55
56    let mut lines: Vec<String> = Vec::with_capacity(HEIGHT + 2);
57    lines.push(if label.is_empty() {
58        "+----------------+".to_string()
59    } else {
60        // Left-justify the label to a 14-char field, matching Python `{:14s}`.
61        format!("+--[{label:<14}]+")
62    });
63
64    for (row_idx, row) in grid.iter().enumerate() {
65        let mut line = String::with_capacity(WIDTH + 2);
66        line.push('|');
67        for (col_idx, &count) in row.iter().enumerate() {
68            if row_idx == start_y && col_idx == start_x {
69                line.push('S');
70            } else if row_idx == end_y && col_idx == end_x {
71                line.push('E');
72            } else {
73                line.push(VALUES[(count as usize).min(VALUES.len() - 1)] as char);
74            }
75        }
76        line.push('|');
77        lines.push(line);
78    }
79
80    lines.push("+----------------+".to_string());
81    lines.join("\n")
82}