Skip to main content

aoc/year2016/
day14.rs

1//! # One-Time Pad
2//!
3//! Brute force slog through all possible keys, parallelized as much as possible. An optimization
4//! for part two is a quick method to convert `u32` to 8 ASCII digits.
5use core::fmt::NumBuffer;
6use std::collections::{BTreeMap, BTreeSet};
7use std::sync::Mutex;
8
9use self::implementation::*;
10use crate::util::md5::*;
11use crate::util::thread::*;
12
13/// Atomics can be safely shared between threads.
14struct Shared<'a> {
15    input: &'a str,
16    part_two: bool,
17    iter: AtomicIter,
18    mutex: Mutex<Exclusive>,
19}
20
21/// Regular data structures need to be protected by a mutex.
22struct Exclusive {
23    threes: BTreeMap<i32, u32>,
24    fives: BTreeMap<i32, u32>,
25    found: BTreeSet<i32>,
26}
27
28pub fn parse(input: &str) -> &str {
29    input.trim()
30}
31
32/// Hash each key once.
33pub fn part1(input: &str) -> i32 {
34    generate_pad(input, false)
35}
36
37/// Hash each key an additional 2016 times.
38pub fn part2(input: &str) -> i32 {
39    generate_pad(input, true)
40}
41
42/// Find the first 64 keys that satisfy the rules.
43fn generate_pad(input: &str, part_two: bool) -> i32 {
44    let step = if cfg!(feature = "simd") { 32 } else { 1 };
45
46    let exclusive =
47        Exclusive { threes: BTreeMap::new(), fives: BTreeMap::new(), found: BTreeSet::new() };
48    let shared =
49        Shared { input, part_two, iter: AtomicIter::new(0, step), mutex: Mutex::new(exclusive) };
50
51    // Use as many cores as possible to parallelize the search.
52    spawn(|| worker(&shared));
53
54    let exclusive = shared.mutex.into_inner().unwrap();
55    *exclusive.found.iter().nth(63).unwrap()
56}
57
58/// Write the salt and integer index as ASCII characters.
59fn format_string(prefix: &str, n: i32) -> ([u8; 64], usize) {
60    let mut number = NumBuffer::new();
61    let digits = n.format_into(&mut number).as_bytes();
62    let size = prefix.len() + digits.len();
63
64    let mut buffer = [0; 64];
65    buffer[..prefix.len()].copy_from_slice(prefix.as_bytes());
66    buffer[prefix.len()..size].copy_from_slice(digits);
67
68    (buffer, size)
69}
70
71/// Quickly convert a `u32` to an array of 8 ASCII values.
72#[inline]
73fn to_ascii(n: u32) -> [u8; 8] {
74    // Spread each nibble into its own byte, for example `1234abcd` becomes `010203040a0b0c0d`.
75    let mut n = n as u64;
76    n = ((n << 16) & 0x0000ffff00000000) | (n & 0x000000000000ffff);
77    n = ((n << 8) & 0x00ff000000ff0000) | (n & 0x000000ff000000ff);
78    n = ((n << 4) & 0x0f000f000f000f00) | (n & 0x000f000f000f000f);
79
80    // If a digit is 0 to 9 then we need to add `0x30` to convert to an ASCII digit.
81    // For digits from 10 to 15 we need to further add `0x27` to convert to lowercase ASCII.
82    // Steps:
83    // * Add 6 to each digit
84    // * If digit is 10 or higher then the highest bit in each nibble will be set
85    // * Shift this bit to create a mask
86    // * Multiply mask by 0x27 to get ASCII conversion offset
87    // For example, mask of `010203040a0b0c0d` is `0000000001010101`.
88
89    let mask = ((n + 0x0606060606060606) >> 4) & 0x0101010101010101;
90    n = n + 0x3030303030303030 + 0x27 * mask;
91    n.to_be_bytes()
92}
93
94#[cfg(not(feature = "simd"))]
95mod implementation {
96    use super::*;
97
98    pub(super) fn worker(shared: &Shared<'_>) {
99        while let Some(n) = shared.iter.next() {
100            // Get the next key to check.
101            let n = n as i32;
102
103            // Calculate the hash.
104            let (mut buffer, size) = format_string(shared.input, n);
105            let mut result = hash(&mut buffer, size);
106
107            if shared.part_two {
108                for _ in 0..2016 {
109                    buffer[0..8].copy_from_slice(&to_ascii(result[0]));
110                    buffer[8..16].copy_from_slice(&to_ascii(result[1]));
111                    buffer[16..24].copy_from_slice(&to_ascii(result[2]));
112                    buffer[24..32].copy_from_slice(&to_ascii(result[3]));
113                    result = hash(&mut buffer, 32);
114                }
115            }
116
117            check(shared, n, result);
118        }
119    }
120
121    /// Check for sequences of 3 or 5 consecutive matching digits.
122    fn check(shared: &Shared<'_>, n: i32, hash: [u32; 4]) {
123        let [a, b, c, d] = hash;
124
125        let mut prev = u32::MAX;
126        let mut same = 1;
127        let mut three = 0;
128        let mut five = 0;
129
130        for mut word in [d, c, b, a] {
131            for _ in 0..8 {
132                let next = word & 0xf;
133
134                if next == prev {
135                    same += 1;
136                } else {
137                    same = 1;
138                }
139
140                if same == 3 {
141                    three = 1 << next;
142                }
143                if same == 5 {
144                    five |= 1 << next;
145                }
146
147                word >>= 4;
148                prev = next;
149            }
150        }
151
152        if three != 0 || five != 0 {
153            let mut exclusive = shared.mutex.lock().unwrap();
154            let mut candidates = Vec::new();
155
156            // Compare against all 5 digit sequences.
157            if three != 0 {
158                exclusive.threes.insert(n, three);
159
160                for (_, mask) in exclusive.fives.range(n + 1..n + 1001) {
161                    if three & mask != 0 {
162                        candidates.push(n);
163                    }
164                }
165            }
166
167            // Compare against all 3 digit sequences.
168            if five != 0 {
169                exclusive.fives.insert(n, five);
170
171                for (&index, &mask) in exclusive.threes.range(n - 1000..n) {
172                    if five & mask != 0 {
173                        candidates.push(index);
174                    }
175                }
176            }
177
178            // Add any matching keys found, finishing once we have at least 64 keys.
179            exclusive.found.extend(candidates);
180
181            if exclusive.found.len() >= 64 {
182                shared.iter.stop();
183            }
184        }
185    }
186}
187
188#[cfg(feature = "simd")]
189mod implementation {
190    use std::simd::prelude::*;
191
192    use super::*;
193    use crate::util::bitset::*;
194    use crate::util::md5::simd::hash_fixed;
195
196    /// Use SIMD to compute hashes in parallel in blocks of 32.
197    pub(super) fn worker(shared: &Shared<'_>) {
198        let mut result = [Simd::splat(0); 4];
199        let mut buffers = [[0; 64]; 32];
200
201        while let Some(start) = shared.iter.next() {
202            // Get the next key to check.
203            let start = start as i32;
204
205            // Calculate the hash.
206            for i in 0..32 {
207                let (mut buffer, size) = format_string(shared.input, start + i as i32);
208                let [a, b, c, d] = hash(&mut buffer, size);
209
210                result[0][i] = a;
211                result[1][i] = b;
212                result[2][i] = c;
213                result[3][i] = d;
214            }
215
216            if shared.part_two {
217                for _ in 0..2016 {
218                    for i in 0..32 {
219                        buffers[i][0..8].copy_from_slice(&to_ascii(result[0][i]));
220                        buffers[i][8..16].copy_from_slice(&to_ascii(result[1][i]));
221                        buffers[i][16..24].copy_from_slice(&to_ascii(result[2][i]));
222                        buffers[i][24..32].copy_from_slice(&to_ascii(result[3][i]));
223                    }
224                    result = hash_fixed(&mut buffers, 32);
225                }
226            }
227
228            check(shared, start, &result);
229        }
230    }
231
232    /// Check for sequences of 3 or 5 consecutive matching digits.
233    #[inline]
234    fn check(shared: &Shared<'_>, start: i32, hash: &[Simd<u32, 32>; 4]) {
235        let &[a, b, c, d] = hash;
236
237        let mut prev: Simd<u32, 32> = Simd::splat(u32::MAX);
238        let mut same: Simd<u32, 32> = Simd::splat(1);
239        let mut three: Simd<u32, 32> = Simd::splat(0);
240        let mut five: Simd<u32, 32> = Simd::splat(0);
241
242        for mut word in [d, c, b, a] {
243            for _ in 0..8 {
244                let next = word & Simd::splat(0xf);
245                same = next.simd_eq(prev).select(same + Simd::splat(1), Simd::splat(1));
246
247                three = same.simd_eq(Simd::splat(3)).select(Simd::splat(1) << next, three);
248                five |= same.simd_eq(Simd::splat(5)).select(Simd::splat(1) << next, Simd::splat(0));
249
250                word >>= 4;
251                prev = next;
252            }
253        }
254
255        let three_mask = three.simd_ne(Simd::splat(0)).to_bitmask();
256        let five_mask = five.simd_ne(Simd::splat(0)).to_bitmask();
257
258        if three_mask != 0 || five_mask != 0 {
259            let mut exclusive = shared.mutex.lock().unwrap();
260            let mut candidates = Vec::new();
261
262            for i in three_mask.biterator() {
263                let three = three[i];
264                let n = start + i as i32;
265
266                // Compare against all 5 digit sequences.
267                if three != 0 {
268                    exclusive.threes.insert(n, three);
269
270                    for (_, mask) in exclusive.fives.range(n + 1..n + 1001) {
271                        if three & mask != 0 {
272                            candidates.push(n);
273                        }
274                    }
275                }
276            }
277
278            for i in five_mask.biterator() {
279                let five = five[i];
280                let n = start + i as i32;
281
282                // Compare against all 3 digit sequences.
283                if five != 0 {
284                    exclusive.fives.insert(n, five);
285
286                    for (&index, &mask) in exclusive.threes.range(n - 1000..n) {
287                        if five & mask != 0 {
288                            candidates.push(index);
289                        }
290                    }
291                }
292            }
293
294            // Add any matching keys found, finishing once we have at least 64 keys.
295            exclusive.found.extend(candidates);
296
297            if exclusive.found.len() >= 64 {
298                shared.iter.stop();
299            }
300        }
301    }
302}