aoc/year2016/
day05.rs

1//! # How About a Nice Game of Chess?
2//!
3//! Essentially, a repeat of [`Year 2015 Day 4`]. We brute force MD5 hashes as quickly as
4//! possible in parallel in blocks of 1000 at a time.
5//!
6//! [`Year 2015 Day 4`]: crate::year2015::day04
7use crate::util::md5::*;
8use crate::util::thread::*;
9use std::sync::Mutex;
10
11struct Shared {
12    prefix: String,
13    iter: AtomicIter,
14    mutex: Mutex<Exclusive>,
15}
16
17struct Exclusive {
18    found: Vec<(u32, u32)>,
19    mask: u16,
20}
21
22pub fn parse(input: &str) -> Vec<u32> {
23    let shared = Shared {
24        prefix: input.trim().to_owned(),
25        iter: AtomicIter::new(1000, 1000),
26        mutex: Mutex::new(Exclusive { found: vec![], mask: 0 }),
27    };
28
29    // Handle the first 999 numbers specially as the number of digits varies.
30    for n in 1..1000 {
31        let (mut buffer, size) = format_string(&shared.prefix, n);
32        check_hash(&mut buffer, size, n, &shared);
33    }
34
35    // Use as many cores as possible to parallelize the remaining search.
36    spawn(|| {
37        #[cfg(not(feature = "simd"))]
38        worker(&shared);
39        #[cfg(feature = "simd")]
40        simd::worker(&shared);
41    });
42
43    let mut found = shared.mutex.into_inner().unwrap().found;
44    found.sort_unstable();
45    found.iter().map(|&(_, n)| n).collect()
46}
47
48pub fn part1(input: &[u32]) -> String {
49    let password = input.iter().take(8).fold(0, |acc, n| (acc << 4) | (n >> 8));
50    format!("{password:08x}")
51}
52
53pub fn part2(input: &[u32]) -> String {
54    let mut password = 0;
55    let mut mask = 0xffffffff;
56
57    for n in input {
58        let sixth = n >> 8;
59        if sixth < 8 {
60            let shift = 4 * (7 - sixth);
61            let seventh = (n >> 4) & 0xf;
62            password |= (seventh << shift) & mask;
63            mask &= !(0xf << shift);
64        }
65    }
66
67    format!("{password:08x}")
68}
69
70fn format_string(prefix: &str, mut n: u32) -> ([u8; 64], usize) {
71    let prefix_len = prefix.len();
72    let digits = n.max(1).ilog10() as usize + 1;
73    let size = prefix_len + digits;
74
75    let mut buffer = [0; 64];
76    buffer[..prefix_len].copy_from_slice(prefix.as_bytes());
77
78    for i in (prefix_len..size).rev() {
79        buffer[i] = b'0' + (n % 10) as u8;
80        n /= 10;
81    }
82
83    (buffer, size)
84}
85
86fn check_hash(buffer: &mut [u8], size: usize, n: u32, shared: &Shared) {
87    let [result, ..] = hash(buffer, size);
88
89    if result & 0xfffff000 == 0 {
90        let mut exclusive = shared.mutex.lock().unwrap();
91
92        exclusive.found.push((n, result));
93        exclusive.mask |= 1 << (result >> 8);
94
95        if exclusive.mask & 0xff == 0xff {
96            shared.iter.stop();
97        }
98    }
99}
100
101#[cfg(not(feature = "simd"))]
102fn worker(shared: &Shared) {
103    while let Some(offset) = shared.iter.next() {
104        let (mut buffer, size) = format_string(&shared.prefix, offset);
105
106        for n in 0..1000 {
107            // Format macro is very slow, so update digits directly
108            buffer[size - 3] = b'0' + (n / 100) as u8;
109            buffer[size - 2] = b'0' + ((n / 10) % 10) as u8;
110            buffer[size - 1] = b'0' + (n % 10) as u8;
111
112            check_hash(&mut buffer, size, offset + n, shared);
113        }
114    }
115}
116
117#[cfg(feature = "simd")]
118mod simd {
119    use super::*;
120    use crate::util::bitset::*;
121    use crate::util::md5::simd::hash_fixed;
122    use std::simd::cmp::SimdPartialEq as _;
123    use std::simd::*;
124
125    #[expect(clippy::needless_range_loop)]
126    fn check_hash_simd<const N: usize>(
127        buffers: &mut [[u8; 64]; N],
128        size: usize,
129        start: u32,
130        offset: u32,
131        shared: &Shared,
132    ) {
133        // Format macro is very slow, so update digits directly
134        for i in 0..N {
135            let n = offset + i as u32;
136            buffers[i][size - 3] = b'0' + (n / 100) as u8;
137            buffers[i][size - 2] = b'0' + ((n / 10) % 10) as u8;
138            buffers[i][size - 1] = b'0' + (n % 10) as u8;
139        }
140
141        let [result, ..] = hash_fixed(buffers, size);
142        let bitmask = (result & Simd::splat(0xfffff000)).simd_eq(Simd::splat(0)).to_bitmask();
143
144        if bitmask != 0 {
145            let mut exclusive = shared.mutex.lock().unwrap();
146
147            for i in bitmask.biterator() {
148                exclusive.found.push((start + offset + i as u32, result[i]));
149                exclusive.mask |= 1 << (result[i] >> 8);
150
151                if exclusive.mask & 0xff == 0xff {
152                    shared.iter.stop();
153                }
154            }
155        }
156    }
157
158    pub(super) fn worker(shared: &Shared) {
159        while let Some(start) = shared.iter.next() {
160            let (prefix, size) = format_string(&shared.prefix, start);
161            let buffers = &mut [prefix; 32];
162
163            for offset in (0..992).step_by(32) {
164                check_hash_simd(buffers, size, start, offset, shared);
165            }
166
167            let buffers = &mut [prefix; 8];
168            check_hash_simd(buffers, size, start, 992, shared);
169        }
170    }
171}