dotscope 0.6.0

A high-performance, cross-platform framework for analyzing and reverse engineering .NET PE executables
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
//! `AssemblyRef` Hash module.
//!
//! This module provides cryptographic hash support for `AssemblyRef` metadata table entries in
//! .NET assemblies. The [`crate::metadata::tables::assemblyref::assemblyrefhash::AssemblyRefHash`]
//! struct encapsulates hash values used for assembly identity verification, supporting both MD5
//! and SHA1 hash algorithms as specified in ECMA-335.
//!
//! # Architecture
//!
//! The module implements cryptographic hash handling for assembly reference verification,
//! providing utilities to create, validate, and format hash values from metadata blob data.
//! Hash algorithm detection is performed automatically based on data length.
//!
//! # Key Components
//!
//! - [`crate::metadata::tables::assemblyref::assemblyrefhash::AssemblyRefHash`] - Main hash wrapper structure
//! - [`crate::metadata::tables::assemblyref::assemblyrefhash::bytes_to_hex`] - Hex formatting utility
//!
//! # Assembly Reference Hashing
//!
//! `AssemblyRef` hash values serve as cryptographic fingerprints for referenced assemblies, enabling:
//! - **Assembly Identity Verification**: Confirming referenced assemblies match expected versions
//! - **Integrity Checking**: Detecting assembly tampering or corruption
//! - **Version Binding**: Ensuring strong name references resolve to correct assemblies
//! - **Security Analysis**: Identifying potentially malicious assembly substitution
//!
//! # Supported Hash Algorithms
//!
//! This module supports the standard hash algorithms used in .NET assemblies:
//! - **MD5**: 128-bit (16 bytes) hash values (legacy, security deprecated)
//! - **SHA1**: 160-bit (20 bytes) hash values (legacy, security deprecated)
//! - **Custom/Unknown**: Other hash lengths for extensibility
//!
//! # Hash Format
//!
//! Hash data is stored as raw bytes in the metadata blob heap, accessed through
//! [`crate::metadata::tables::assemblyref::AssemblyRef`] entries. The hash algorithm is identified
//! by examining the hash length and cross-referencing with assembly metadata.
//!
//! # Security Considerations
//!
//! Both MD5 and SHA1 are cryptographically broken and should not be used for new applications.
//! Modern .NET assemblies use SHA256 or stronger algorithms. This module supports legacy
//! algorithms for compatibility with older assemblies and forensic analysis.
//!
//! # Thread Safety
//!
//! All operations are thread-safe and do not modify shared state. Hash verification operations
//! create temporary hasher instances and do not affect global state.
//!
//! # Integration
//!
//! This module integrates with:
//! - [`crate::metadata::tables::assemblyref`] - `AssemblyRef` table entries that reference hash data
//! - [`crate::metadata::streams::Blob`] - Blob heap storage for hash data
//! - [`crate::metadata::tables::assembly`] - Hash algorithm identifiers
//!
//! # References
//!
//! - [ECMA-335 II.23.1.16](https://ecma-international.org/wp-content/uploads/ECMA-335_6th_edition_june_2012.pdf) - `AssemblyRef` table specification
//! - [RFC 1321](https://tools.ietf.org/html/rfc1321) - MD5 Message-Digest Algorithm (deprecated)
//! - [RFC 3174](https://tools.ietf.org/html/rfc3174) - SHA-1 Hash Function (deprecated)

#[cfg(feature = "legacy-crypto")]
use crate::utils::{compute_md5, compute_sha1};
use crate::utils::{compute_sha256, compute_sha384, compute_sha512};
use crate::Result;
use std::fmt::Write;

/// Convert bytes to lowercase hexadecimal string representation
///
/// Utility function that transforms raw bytes into a lowercase hexadecimal string.
/// Each byte is converted to exactly two lowercase hex characters.
///
/// # Arguments
/// * `bytes` - Slice of bytes to convert
///
/// # Returns
/// String with lowercase hex representation. Length is `bytes.len() * 2`.
///
/// # Performance
/// Pre-allocates output string with exact capacity to avoid reallocations.
fn bytes_to_hex(bytes: &[u8]) -> String {
    let mut hex_string = String::with_capacity(bytes.len() * 2);
    for byte in bytes {
        let _ = write!(&mut hex_string, "{byte:02x}");
    }
    hex_string
}

/// Cryptographic hash for `AssemblyRef` metadata table entries
///
/// Encapsulates hash values used for assembly identity verification and integrity checking
/// in .NET assembly references. Supports MD5 (16 bytes) and SHA1 (20 bytes) hash algorithms
/// as commonly found in .NET assembly metadata, with extensibility for custom hash formats.
///
/// Hash data originates from the blob heap and serves as a cryptographic fingerprint
/// for referenced assemblies, enabling strong-name binding and tamper detection.
///
/// # Hash Algorithm Detection
///
/// The hash algorithm is inferred from the data length:
/// - **16 bytes**: MD5 hash (legacy, cryptographically broken)
/// - **20 bytes**: SHA1 hash (legacy, cryptographically broken)
/// - **Other lengths**: Custom or unknown hash algorithms
///
/// # Security Notice
///
/// Both MD5 and SHA1 are cryptographically compromised and should not be used for
/// security-critical applications. This implementation exists for compatibility with
/// legacy .NET assemblies and forensic analysis purposes.
#[derive(Debug)]
pub struct AssemblyRefHash {
    /// Raw hash bytes (MD5, SHA1, or other)
    data: Vec<u8>,
}

impl AssemblyRefHash {
    /// Create a new `AssemblyRefHash` from hash data bytes
    ///
    /// Constructs an `AssemblyRefHash` instance from raw hash bytes, typically obtained
    /// from the metadata blob heap. The hash algorithm is inferred from the data length.
    ///
    /// # Arguments
    /// * `data` - Raw hash bytes from the blob heap
    ///
    /// # Returns
    /// * `Ok(AssemblyRefHash)` - Successfully created hash wrapper
    /// * `Err(Error)` - If input data is empty (invalid per ECMA-335)
    ///
    /// # Errors
    /// Returns [`crate::Error`] if the input data is empty, as `AssemblyRef` hash entries
    /// are required to contain actual hash data per ECMA-335 specification.
    pub fn new(data: &[u8]) -> Result<AssemblyRefHash> {
        if data.is_empty() {
            return Err(malformed_error!(
                "AssemblyRefHash entries are not allowed to be empty"
            ));
        }

        Ok(AssemblyRefHash {
            data: data.to_vec(),
        })
    }

    /// Get the underlying hash data bytes
    ///
    /// Returns a reference to the raw hash bytes stored in this instance. The data
    /// represents the cryptographic hash value as stored in the assembly metadata.
    ///
    /// # Returns
    /// Slice containing the raw hash bytes. Length indicates hash algorithm:
    /// - 16 bytes: MD5 hash
    /// - 20 bytes: SHA1 hash  
    /// - Other: Custom/unknown hash algorithm
    #[must_use]
    pub fn data(&self) -> &[u8] {
        &self.data
    }

    /// Get a lowercase hexadecimal representation of the hash
    ///
    /// Converts the hash bytes to a lowercase hexadecimal string representation,
    /// suitable for display, logging, and comparison operations.
    ///
    /// # Returns
    /// String containing lowercase hexadecimal representation of hash bytes.
    /// Length is exactly `data().len() * 2` characters.
    #[must_use]
    pub fn hex(&self) -> String {
        bytes_to_hex(&self.data)
    }

    /// Get a human-readable string representation with algorithm identification
    ///
    /// Returns a formatted string that includes both the detected hash algorithm
    /// and the hexadecimal hash value, suitable for debugging and user display.
    ///
    /// Algorithm detection is based on hash length:
    /// - 16 bytes: "MD5: {hex}"
    /// - 20 bytes: "SHA1: {hex}"
    /// - 32 bytes: "SHA256: {hex}"
    /// - 48 bytes: "SHA384: {hex}"
    /// - 64 bytes: "SHA512: {hex}"
    /// - Other: "Unknown: {hex}"
    ///
    /// # Returns
    /// Formatted string with algorithm prefix and lowercase hex hash value.
    #[must_use]
    pub fn to_string_pretty(&self) -> String {
        let hex = self.hex();
        let algorithm = match self.data.len() {
            16 => "MD5",
            20 => "SHA1",
            32 => "SHA256",
            48 => "SHA384",
            64 => "SHA512",
            _ => "Unknown",
        };

        format!("{algorithm}: {hex}")
    }

    /// Verify if this hash matches input data using MD5 algorithm
    ///
    /// Computes the MD5 hash of the provided input data and compares it against
    /// the stored hash value. Only valid for 16-byte (MD5) hashes.
    ///
    /// **Security Warning**: MD5 is cryptographically broken and should not be used
    /// for security purposes. This method exists for compatibility with legacy assemblies.
    ///
    /// # Arguments
    /// * `expected` - Input data to hash and verify against stored hash
    ///
    /// # Returns
    /// * `true` - Hash matches (stored hash is 16 bytes and MD5 computation matches)
    /// * `false` - Hash doesn't match, stored hash is not 16 bytes, or `legacy-crypto` feature is disabled
    #[must_use]
    #[cfg(feature = "legacy-crypto")]
    pub fn verify_md5(&self, expected: &[u8]) -> bool {
        if self.data.len() != 16 {
            return false;
        }
        self.data == compute_md5(expected)
    }

    /// Verify if this hash matches input data using MD5 algorithm - stub when legacy-crypto is disabled.
    #[must_use]
    #[cfg(not(feature = "legacy-crypto"))]
    pub fn verify_md5(&self, _expected: &[u8]) -> bool {
        false // Cannot verify without MD5 support
    }

    /// Verify if this hash matches input data using SHA1 algorithm
    ///
    /// Computes the SHA1 hash of the provided input data and compares it against
    /// the stored hash value. Only valid for 20-byte (SHA1) hashes.
    ///
    /// **Security Warning**: SHA1 is cryptographically broken and should not be used
    /// for security purposes. This method exists for compatibility with legacy assemblies.
    ///
    /// # Arguments
    /// * `expected` - Input data to hash and verify against stored hash
    ///
    /// # Returns
    /// * `true` - Hash matches (stored hash is 20 bytes and SHA1 computation matches)
    /// * `false` - Hash doesn't match, stored hash is not 20 bytes, or `legacy-crypto` feature is disabled
    #[must_use]
    #[cfg(feature = "legacy-crypto")]
    pub fn verify_sha1(&self, expected: &[u8]) -> bool {
        if self.data.len() != 20 {
            return false;
        }
        self.data == compute_sha1(expected)
    }

    /// Verify if this hash matches input data using SHA1 algorithm - stub when legacy-crypto is disabled.
    #[must_use]
    #[cfg(not(feature = "legacy-crypto"))]
    pub fn verify_sha1(&self, _expected: &[u8]) -> bool {
        false // Cannot verify without SHA1 support
    }

    /// Verify if this hash matches input data using SHA256 algorithm
    ///
    /// Computes the SHA256 hash of the provided input data and compares it against
    /// the stored hash value. Only valid for 32-byte (SHA256) hashes.
    ///
    /// # Arguments
    /// * `expected` - Input data to hash and verify against stored hash
    ///
    /// # Returns
    /// * `true` - Hash matches (stored hash is 32 bytes and SHA256 computation matches)
    /// * `false` - Hash doesn't match or stored hash is not 32 bytes
    #[must_use]
    pub fn verify_sha256(&self, expected: &[u8]) -> bool {
        if self.data.len() != 32 {
            return false;
        }
        self.data == compute_sha256(expected)
    }

    /// Verify if this hash matches input data using SHA384 algorithm
    ///
    /// Computes the SHA384 hash of the provided input data and compares it against
    /// the stored hash value. Only valid for 48-byte (SHA384) hashes.
    ///
    /// # Arguments
    /// * `expected` - Input data to hash and verify against stored hash
    ///
    /// # Returns
    /// * `true` - Hash matches (stored hash is 48 bytes and SHA384 computation matches)
    /// * `false` - Hash doesn't match or stored hash is not 48 bytes
    #[must_use]
    pub fn verify_sha384(&self, expected: &[u8]) -> bool {
        if self.data.len() != 48 {
            return false;
        }
        self.data == compute_sha384(expected)
    }

    /// Verify if this hash matches input data using SHA512 algorithm
    ///
    /// Computes the SHA512 hash of the provided input data and compares it against
    /// the stored hash value. Only valid for 64-byte (SHA512) hashes.
    ///
    /// # Arguments
    /// * `expected` - Input data to hash and verify against stored hash
    ///
    /// # Returns
    /// * `true` - Hash matches (stored hash is 64 bytes and SHA512 computation matches)
    /// * `false` - Hash doesn't match or stored hash is not 64 bytes
    #[must_use]
    pub fn verify_sha512(&self, expected: &[u8]) -> bool {
        if self.data.len() != 64 {
            return false;
        }
        self.data == compute_sha512(expected)
    }
}

/// Tests that work without legacy-crypto (SHA-2 and basic functionality)
#[cfg(test)]
mod tests {
    use super::*;

    // Helper function to create test SHA256 hash
    fn create_test_sha256_hash() -> Vec<u8> {
        compute_sha256(b"test data")
    }

    // Helper function to create test SHA384 hash
    fn create_test_sha384_hash() -> Vec<u8> {
        compute_sha384(b"test data")
    }

    // Helper function to create test SHA512 hash
    fn create_test_sha512_hash() -> Vec<u8> {
        compute_sha512(b"test data")
    }

    #[test]
    fn test_new_with_valid_data() {
        let data = vec![1, 2, 3, 4, 5];
        let hash = AssemblyRefHash::new(&data).unwrap();
        assert_eq!(hash.data(), &data);
    }

    #[test]
    fn test_new_with_empty_data_fails() {
        let result = AssemblyRefHash::new(&[]);
        assert!(result.is_err());
        let error_msg = result.unwrap_err().to_string();
        assert!(error_msg.contains("not allowed to be empty"));
    }

    #[test]
    fn test_data_getter() {
        let test_data = vec![0x12, 0x34, 0x56, 0x78];
        let hash = AssemblyRefHash::new(&test_data).unwrap();
        assert_eq!(hash.data(), &test_data);
    }

    #[test]
    fn test_hex_formatting() {
        let test_data = vec![0x12, 0x34, 0x56, 0x78, 0xab, 0xcd, 0xef];
        let hash = AssemblyRefHash::new(&test_data).unwrap();
        assert_eq!(hash.hex(), "12345678abcdef");
    }

    #[test]
    fn test_hex_formatting_with_zeros() {
        let test_data = vec![0x00, 0x01, 0x0a, 0xff];
        let hash = AssemblyRefHash::new(&test_data).unwrap();
        assert_eq!(hash.hex(), "00010aff");
    }

    #[test]
    fn test_to_string_pretty_md5_length() {
        // 16 bytes should be detected as MD5
        let md5_length_hash = vec![0x42; 16];
        let hash = AssemblyRefHash::new(&md5_length_hash).unwrap();
        let pretty = hash.to_string_pretty();
        assert!(pretty.starts_with("MD5: "));
        assert_eq!(pretty.len(), 5 + 32); // "MD5: " + 32 hex chars
    }

    #[test]
    fn test_to_string_pretty_sha1_length() {
        // 20 bytes should be detected as SHA1
        let sha1_length_hash = vec![0x42; 20];
        let hash = AssemblyRefHash::new(&sha1_length_hash).unwrap();
        let pretty = hash.to_string_pretty();
        assert!(pretty.starts_with("SHA1: "));
        assert_eq!(pretty.len(), 6 + 40); // "SHA1: " + 40 hex chars
    }

    #[test]
    fn test_to_string_pretty_unknown_length() {
        let unknown_hash = vec![1, 2, 3, 4, 5]; // 5 bytes, not a recognized hash length
        let hash = AssemblyRefHash::new(&unknown_hash).unwrap();
        let pretty = hash.to_string_pretty();
        assert!(pretty.starts_with("Unknown: "));
        assert_eq!(pretty, "Unknown: 0102030405");
    }

    #[test]
    fn test_to_string_pretty_sha256() {
        let sha256_hash = create_test_sha256_hash();
        let hash = AssemblyRefHash::new(&sha256_hash).unwrap();
        let pretty = hash.to_string_pretty();
        assert!(pretty.starts_with("SHA256: "));
        assert_eq!(pretty.len(), 8 + 64); // "SHA256: " + 64 hex chars
    }

    #[test]
    fn test_to_string_pretty_sha384() {
        let sha384_hash = create_test_sha384_hash();
        let hash = AssemblyRefHash::new(&sha384_hash).unwrap();
        let pretty = hash.to_string_pretty();
        assert!(pretty.starts_with("SHA384: "));
        assert_eq!(pretty.len(), 8 + 96); // "SHA384: " + 96 hex chars
    }

    #[test]
    fn test_to_string_pretty_sha512() {
        let sha512_hash = create_test_sha512_hash();
        let hash = AssemblyRefHash::new(&sha512_hash).unwrap();
        let pretty = hash.to_string_pretty();
        assert!(pretty.starts_with("SHA512: "));
        assert_eq!(pretty.len(), 8 + 128); // "SHA512: " + 128 hex chars
    }

    #[test]
    fn test_verify_sha256_success() {
        let test_input = b"test data";
        let expected_hash = create_test_sha256_hash();
        let hash = AssemblyRefHash::new(&expected_hash).unwrap();

        assert!(hash.verify_sha256(test_input));
    }

    #[test]
    fn test_verify_sha256_failure_wrong_data() {
        let expected_hash = create_test_sha256_hash();
        let hash = AssemblyRefHash::new(&expected_hash).unwrap();

        assert!(!hash.verify_sha256(b"wrong data"));
    }

    #[test]
    fn test_verify_sha256_failure_wrong_length() {
        // 20 bytes, not 32
        let wrong_length_hash = vec![0x42; 20];
        let hash = AssemblyRefHash::new(&wrong_length_hash).unwrap();

        assert!(!hash.verify_sha256(b"test data"));
    }

    #[test]
    fn test_verify_sha384_success() {
        let test_input = b"test data";
        let expected_hash = create_test_sha384_hash();
        let hash = AssemblyRefHash::new(&expected_hash).unwrap();

        assert!(hash.verify_sha384(test_input));
    }

    #[test]
    fn test_verify_sha384_failure_wrong_data() {
        let expected_hash = create_test_sha384_hash();
        let hash = AssemblyRefHash::new(&expected_hash).unwrap();

        assert!(!hash.verify_sha384(b"wrong data"));
    }

    #[test]
    fn test_verify_sha384_failure_wrong_length() {
        let sha256_hash = create_test_sha256_hash(); // 32 bytes, not 48
        let hash = AssemblyRefHash::new(&sha256_hash).unwrap();

        assert!(!hash.verify_sha384(b"test data"));
    }

    #[test]
    fn test_verify_sha512_success() {
        let test_input = b"test data";
        let expected_hash = create_test_sha512_hash();
        let hash = AssemblyRefHash::new(&expected_hash).unwrap();

        assert!(hash.verify_sha512(test_input));
    }

    #[test]
    fn test_verify_sha512_failure_wrong_data() {
        let expected_hash = create_test_sha512_hash();
        let hash = AssemblyRefHash::new(&expected_hash).unwrap();

        assert!(!hash.verify_sha512(b"wrong data"));
    }

    #[test]
    fn test_verify_sha512_failure_wrong_length() {
        let sha384_hash = create_test_sha384_hash(); // 48 bytes, not 64
        let hash = AssemblyRefHash::new(&sha384_hash).unwrap();

        assert!(!hash.verify_sha512(b"test data"));
    }

    #[test]
    fn test_with_real_sha256_hash() {
        let input = b"The quick brown fox jumps over the lazy dog";
        let expected_hash = compute_sha256(input);

        let hash = AssemblyRefHash::new(&expected_hash).unwrap();
        assert_eq!(hash.data().len(), 32);
        assert!(hash.verify_sha256(input));

        let pretty = hash.to_string_pretty();
        assert!(pretty.starts_with("SHA256: "));
    }

    #[test]
    fn test_with_real_sha384_hash() {
        let input = b"The quick brown fox jumps over the lazy dog";
        let expected_hash = compute_sha384(input);

        let hash = AssemblyRefHash::new(&expected_hash).unwrap();
        assert_eq!(hash.data().len(), 48);
        assert!(hash.verify_sha384(input));
        assert!(!hash.verify_sha256(input)); // Wrong algorithm

        let pretty = hash.to_string_pretty();
        assert!(pretty.starts_with("SHA384: "));
    }

    #[test]
    fn test_with_real_sha512_hash() {
        let input = b"The quick brown fox jumps over the lazy dog";
        let expected_hash = compute_sha512(input);

        let hash = AssemblyRefHash::new(&expected_hash).unwrap();
        assert_eq!(hash.data().len(), 64);
        assert!(hash.verify_sha512(input));
        assert!(!hash.verify_sha384(input)); // Wrong algorithm

        let pretty = hash.to_string_pretty();
        assert!(pretty.starts_with("SHA512: "));
    }

    #[test]
    fn test_bytes_to_hex_helper() {
        let bytes = vec![0x00, 0x01, 0x0a, 0x10, 0xff];
        let hex = bytes_to_hex(&bytes);
        assert_eq!(hex, "00010a10ff");
    }

    #[test]
    fn test_bytes_to_hex_empty() {
        let hex = bytes_to_hex(&[]);
        assert_eq!(hex, "");
    }

    #[test]
    fn test_edge_case_single_byte() {
        let single_byte = vec![0x42];
        let hash = AssemblyRefHash::new(&single_byte).unwrap();
        assert_eq!(hash.hex(), "42");
        assert_eq!(hash.to_string_pretty(), "Unknown: 42");
    }

    #[test]
    fn test_edge_case_max_byte_values() {
        // Use 31 bytes to get "Unknown" (not a recognized hash length)
        let max_bytes = vec![0xff; 31];
        let hash = AssemblyRefHash::new(&max_bytes).unwrap();
        assert_eq!(hash.hex(), "f".repeat(62));
        assert!(hash.to_string_pretty().starts_with("Unknown: "));
    }

    #[test]
    fn test_case_sensitivity_in_hex() {
        let test_data = vec![0xab, 0xcd, 0xef];
        let hash = AssemblyRefHash::new(&test_data).unwrap();
        let hex = hash.hex();
        // Verify all hex characters are lowercase
        assert_eq!(hex, "abcdef");
        assert!(!hex.contains('A'));
        assert!(!hex.contains('B'));
        assert!(!hex.contains('C'));
        assert!(!hex.contains('D'));
        assert!(!hex.contains('E'));
        assert!(!hex.contains('F'));
    }
}

/// Tests that require legacy-crypto (MD5/SHA1 verification)
#[cfg(all(test, feature = "legacy-crypto"))]
mod legacy_tests {
    use super::*;

    // Helper function to create test MD5 hash
    fn create_test_md5_hash() -> Vec<u8> {
        compute_md5(b"test data")
    }

    // Helper function to create test SHA1 hash
    fn create_test_sha1_hash() -> Vec<u8> {
        compute_sha1(b"test data")
    }

    #[test]
    fn test_verify_md5_success() {
        let test_input = b"test data";
        let expected_hash = create_test_md5_hash();
        let hash = AssemblyRefHash::new(&expected_hash).unwrap();

        assert!(hash.verify_md5(test_input));
    }

    #[test]
    fn test_verify_md5_failure_wrong_data() {
        let expected_hash = create_test_md5_hash();
        let hash = AssemblyRefHash::new(&expected_hash).unwrap();

        assert!(!hash.verify_md5(b"wrong data"));
    }

    #[test]
    fn test_verify_md5_failure_wrong_length() {
        let sha1_hash = create_test_sha1_hash(); // 20 bytes, not 16
        let hash = AssemblyRefHash::new(&sha1_hash).unwrap();

        assert!(!hash.verify_md5(b"test data"));
    }

    #[test]
    fn test_verify_sha1_success() {
        let test_input = b"test data";
        let expected_hash = create_test_sha1_hash();
        let hash = AssemblyRefHash::new(&expected_hash).unwrap();

        assert!(hash.verify_sha1(test_input));
    }

    #[test]
    fn test_verify_sha1_failure_wrong_data() {
        let expected_hash = create_test_sha1_hash();
        let hash = AssemblyRefHash::new(&expected_hash).unwrap();

        assert!(!hash.verify_sha1(b"wrong data"));
    }

    #[test]
    fn test_verify_sha1_failure_wrong_length() {
        let md5_hash = create_test_md5_hash(); // 16 bytes, not 20
        let hash = AssemblyRefHash::new(&md5_hash).unwrap();

        assert!(!hash.verify_sha1(b"test data"));
    }

    #[test]
    fn test_with_real_md5_hash() {
        let input = b"The quick brown fox jumps over the lazy dog";
        let expected_hash = compute_md5(input);

        let hash = AssemblyRefHash::new(&expected_hash).unwrap();
        assert_eq!(hash.data().len(), 16);
        assert!(hash.verify_md5(input));
        assert!(!hash.verify_sha1(input)); // Wrong algorithm

        let pretty = hash.to_string_pretty();
        assert!(pretty.starts_with("MD5: "));
    }

    #[test]
    fn test_with_real_sha1_hash() {
        let input = b"The quick brown fox jumps over the lazy dog";
        let expected_hash = compute_sha1(input);

        let hash = AssemblyRefHash::new(&expected_hash).unwrap();
        assert_eq!(hash.data().len(), 20);
        assert!(hash.verify_sha1(input));
        assert!(!hash.verify_md5(input)); // Wrong algorithm

        let pretty = hash.to_string_pretty();
        assert!(pretty.starts_with("SHA1: "));
    }

    #[test]
    fn test_edge_case_single_byte_legacy() {
        let single_byte = vec![0x42];
        let hash = AssemblyRefHash::new(&single_byte).unwrap();
        assert!(!hash.verify_md5(b"anything"));
        assert!(!hash.verify_sha1(b"anything"));
    }
}