diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7a1d00a4faa..35a0f43b13f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -94,6 +94,7 @@ add_library(migraphx load_save.cpp logger.cpp make_op.cpp + md5.cpp memory_coloring.cpp module.cpp msgpack.cpp diff --git a/src/include/migraphx/md5.hpp b/src/include/migraphx/md5.hpp new file mode 100644 index 00000000000..760ff34f8a5 --- /dev/null +++ b/src/include/migraphx/md5.hpp @@ -0,0 +1,40 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015-2026 Advanced Micro Devices, Inc. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef MIGRAPHX_GUARD_RTGLIB_MD5_HPP +#define MIGRAPHX_GUARD_RTGLIB_MD5_HPP + +#include +#include +#include + +namespace migraphx { +inline namespace MIGRAPHX_INLINE_NS { + +/// Compute the MD5 digest of a string and return it as a lowercase hex string. +std::string MIGRAPHX_EXPORT md5(const std::string_view& str); + +} // namespace MIGRAPHX_INLINE_NS +} // namespace migraphx + +#endif diff --git a/src/include/migraphx/stringutils.hpp b/src/include/migraphx/stringutils.hpp index fb778aa28b4..dba49cf2829 100644 --- a/src/include/migraphx/stringutils.hpp +++ b/src/include/migraphx/stringutils.hpp @@ -25,12 +25,17 @@ #define MIGRAPHX_GUARD_MIGRAPHLIB_STRINGUTILS_HPP #include +#include +#include #include #include #include +#include #include #include +#include #include +#include namespace migraphx { inline namespace MIGRAPHX_INLINE_NS { @@ -236,6 +241,38 @@ inline auto to_hex_float(const T& x) return ss.str(); } +/// Concatenate the lowercase hex representation of each integer in a range. +/// Each element emits 2*sizeof(element) characters. When lsb is false the +/// most-significant byte is emitted first (standard hex notation); when true, +/// the least-significant byte is first (matches byte-stream hash digests). +template +inline std::string to_hex_string(const Range& r, bool lsb = false) +{ + constexpr std::array hex_digits = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + return std::accumulate(r.begin(), r.end(), std::string{}, [&](std::string acc, const auto& x) { + using type = std::make_unsigned_t>; + const auto u = bit_cast(x); + const auto to_byte = [&](std::ptrdiff_t b) -> std::uint8_t { + return (u >> (b * 8u)) & 0xffu; + }; + const auto append_hex = [&](std::string s, std::uint8_t byte) { + s.push_back(hex_digits[byte >> 4u]); + s.push_back(hex_digits[byte & 0x0fu]); + return s; + }; + const auto bytes = range(sizeof(type)); + if(lsb) + { + return transform_accumulate( + bytes.begin(), bytes.end(), std::move(acc), append_hex, to_byte); + } + const auto rbytes = reverse(bytes); + return transform_accumulate( + rbytes.begin(), rbytes.end(), std::move(acc), append_hex, to_byte); + }); +} + } // namespace MIGRAPHX_INLINE_NS } // namespace migraphx diff --git a/src/md5.cpp b/src/md5.cpp new file mode 100644 index 00000000000..639171d9ff8 --- /dev/null +++ b/src/md5.cpp @@ -0,0 +1,159 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015-2026 Advanced Micro Devices, Inc. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include +#include +#include +#include +#include +#include +#include + +namespace migraphx { +inline namespace MIGRAPHX_INLINE_NS { + +namespace { + +constexpr std::size_t block_size = 64; + +// Per-round shift amounts (RFC 1321 section 3.4). +constexpr std::array shifts = { + 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, + 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, + 4, 11, 16, 23, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21}; + +// Sine-derived constants: floor(2^32 * abs(sin(i + 1))), i = 0..63. +constexpr std::array sine_table = { + 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, + 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, + 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, + 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, + 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, + 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, + 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, + 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391}; + +constexpr std::uint32_t rotate_left(std::uint32_t x, std::uint32_t n) +{ + return (x << n) | (x >> (32u - n)); +} + +template +constexpr std::uint32_t load_le32(It p) +{ + return std::uint32_t{p[0]} | (std::uint32_t{p[1]} << 8u) | (std::uint32_t{p[2]} << 16u) | + (std::uint32_t{p[3]} << 24u); +} + +std::array process_block(std::array state, + std::array block) +{ + std::array m{}; + const auto word_indices = range(m.size()); + std::transform(word_indices.begin(), word_indices.end(), m.begin(), [&](std::ptrdiff_t i) { + return load_le32(block.begin() + (i * 4)); + }); + + // Each round writes the freshly computed value into slot a, then + // std::rotate shifts v right by one so the structured bindings realign + // onto the canonical MD5 register carousel for the next iteration: + // a <- d, b <- a (the just-computed value), c <- b, d <- c. + std::array v = state; + auto& [a, b, c, d] = v; + + for(std::uint32_t i = 0; i < 64; ++i) + { + std::array fg{}; + if(i < 16) + { + fg = {(b & c) | ((~b) & d), i}; + } + else if(i < 32) + { + fg = {(d & b) | ((~d) & c), (5u * i + 1u) % 16u}; + } + else if(i < 48) + { + fg = {b ^ c ^ d, (3u * i + 5u) % 16u}; + } + else + { + fg = {c ^ (b | (~d)), (7u * i) % 16u}; + } + + a = b + rotate_left(a + fg[0] + sine_table[i] + m[fg[1]], shifts[i]); + std::rotate(v.begin(), v.end() - 1, v.end()); + } + + return {state[0] + a, state[1] + b, state[2] + c, state[3] + d}; +} + +std::uint8_t to_uint8(char c) { return bit_cast(c); } + +} // namespace + +std::string md5(const std::string_view& str) +{ + std::array state = {0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476}; + + const std::size_t full_blocks = str.size() / block_size; + const std::size_t remainder = str.size() % block_size; + + std::array block{}; + for(std::size_t i = 0; i < full_blocks; ++i) + { + const auto chunk_begin = str.begin() + (i * block_size); + std::transform(chunk_begin, chunk_begin + block_size, block.begin(), &to_uint8); + state = process_block(state, block); + } + + // Final block(s): remaining bytes, a 0x80 terminator, zero fill, and the + // message bit length in the last 8 bytes (little-endian). Two blocks are + // needed when the bit-length field no longer fits in the current block. + std::array, 2> tail{}; + const auto tail_src_begin = str.begin() + (full_blocks * block_size); + std::transform(tail_src_begin, str.end(), tail[0].begin(), &to_uint8); + tail[0][remainder] = 0x80; + + const bool need_two = (remainder >= block_size - 8); + const std::uint64_t bit_length = std::uint64_t{str.size()} * 8u; + const auto& last = need_two ? tail[1] : tail[0]; + const auto bit_indices = range(8); + transform_partial_sum( + bit_indices.begin(), + bit_indices.end(), + last.end() - 8, + [](std::uint64_t acc, std::uint64_t) { return acc >> 8u; }, + [&](auto) { return bit_length; }); + + state = process_block(state, tail[0]); + if(need_two) + { + state = process_block(state, tail[1]); + } + + return to_hex_string(state, true); +} + +} // namespace MIGRAPHX_INLINE_NS +} // namespace migraphx diff --git a/test/md5.cpp b/test/md5.cpp new file mode 100644 index 00000000000..f8d9ac1452c --- /dev/null +++ b/test/md5.cpp @@ -0,0 +1,165 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015-2026 Advanced Micro Devices, Inc. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include +#include "test.hpp" +#include +#include + +// RFC 1321 test vectors (appendix A.5). +TEST_CASE(md5_rfc_empty) { EXPECT(migraphx::md5("") == "d41d8cd98f00b204e9800998ecf8427e"); } + +TEST_CASE(md5_rfc_a) { EXPECT(migraphx::md5("a") == "0cc175b9c0f1b6a831c399e269772661"); } + +TEST_CASE(md5_rfc_abc) { EXPECT(migraphx::md5("abc") == "900150983cd24fb0d6963f7d28e17f72"); } + +TEST_CASE(md5_rfc_message_digest) +{ + EXPECT(migraphx::md5("message digest") == "f96b697d7cb7938d525a2f31aaf161d0"); +} + +TEST_CASE(md5_rfc_lowercase_alphabet) +{ + EXPECT(migraphx::md5("abcdefghijklmnopqrstuvwxyz") == "c3fcd3d76192e4007dfb496cca67e13b"); +} + +TEST_CASE(md5_rfc_alphanumeric) +{ + EXPECT(migraphx::md5("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") == + "d174ab98d277d9f5a5611c2c9f419d9f"); +} + +TEST_CASE(md5_rfc_digits) +{ + EXPECT(migraphx::md5("1234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890") == + "57edf4a22be3c955ac49da2e2107b67a"); +} + +// Well-known extra test strings. +TEST_CASE(md5_quick_brown_fox) +{ + EXPECT(migraphx::md5("The quick brown fox jumps over the lazy dog") == + "9e107d9d372bb6826bd81d3542a419d6"); +} + +TEST_CASE(md5_quick_brown_fox_period) +{ + EXPECT(migraphx::md5("The quick brown fox jumps over the lazy dog.") == + "e4d909c290d0fb1ca068ffaddf22cbd0"); +} + +// Block boundary tests. MD5 processes in 64-byte blocks; the bit length field +// must fit in the trailing 8 bytes of the last padding block, so input length +// modulo 64 being 55/56/63/64 exercises all padding branches. +TEST_CASE(md5_length_55) +{ + EXPECT(migraphx::md5(std::string(55, 'a')) == "ef1772b6dff9a122358552954ad0df65"); +} + +TEST_CASE(md5_length_56) +{ + EXPECT(migraphx::md5(std::string(56, 'a')) == "3b0c8ac703f828b04c6c197006d17218"); +} + +TEST_CASE(md5_length_63) +{ + EXPECT(migraphx::md5(std::string(63, 'a')) == "b06521f39153d618550606be297466d5"); +} + +TEST_CASE(md5_length_64) +{ + EXPECT(migraphx::md5(std::string(64, 'a')) == "014842d480b571495a4a0363793f7367"); +} + +TEST_CASE(md5_length_65) +{ + EXPECT(migraphx::md5(std::string(65, 'a')) == "c743a45e0d2e6a95cb859adae0248435"); +} + +TEST_CASE(md5_length_119) +{ + EXPECT(migraphx::md5(std::string(119, 'a')) == "8a7bd0732ed6a28ce75f6dabc90e1613"); +} + +TEST_CASE(md5_length_120) +{ + EXPECT(migraphx::md5(std::string(120, 'a')) == "5f61c0ccad4cac44c75ff505e1f1e537"); +} + +// Embedded NUL bytes must be hashed, not treated as terminators. +TEST_CASE(md5_embedded_null) +{ + const std::string s{"a\0b", 3}; + EXPECT(migraphx::md5(s) == "70350f6027bce3713f6b76473084309b"); +} + +TEST_CASE(md5_trailing_null) +{ + EXPECT(migraphx::md5(std::string(1, '\0')) == "93b885adfe0da089cdf634904fd59f71"); +} + +// High-byte input — make sure bytes aren't sign-extended. +TEST_CASE(md5_high_bytes) +{ + const std::string s(16, '\xff'); + EXPECT(migraphx::md5(s) == "8d79cbc9a4ecdde112fc91ba625b13c2"); +} + +// Output shape: 32 lowercase hex characters. +TEST_CASE(md5_output_format) +{ + const std::string hash = migraphx::md5("anything"); + EXPECT(hash.size() == 32); + for(char c : hash) + { + const bool is_hex = (c >= '0' and c <= '9') or (c >= 'a' and c <= 'f'); + EXPECT(is_hex); + } +} + +// Determinism: same input yields same digest every call. +TEST_CASE(md5_deterministic) +{ + const std::string input = "repeatable input"; + EXPECT(migraphx::md5(input) == migraphx::md5(input)); +} + +// Small perturbations of input yield different digests (avalanche sanity check). +TEST_CASE(md5_distinct_outputs) +{ + EXPECT(migraphx::md5("abc") != migraphx::md5("abd")); + EXPECT(migraphx::md5("abc") != migraphx::md5("ABC")); + EXPECT(migraphx::md5("") != migraphx::md5(" ")); + EXPECT(migraphx::md5("a") != migraphx::md5("aa")); +} + +// Accepts string_view constructed from char buffer. +TEST_CASE(md5_string_view_from_buffer) +{ + const char buf[] = "abc"; + const std::string_view view = {buf, 3}; + EXPECT(migraphx::md5(view) == "900150983cd24fb0d6963f7d28e17f72"); +} + +int main(int argc, const char* argv[]) { test::run(argc, argv); } diff --git a/test/stringutils.cpp b/test/stringutils.cpp index cfb48499ab3..53b46b1dadd 100644 --- a/test/stringutils.cpp +++ b/test/stringutils.cpp @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Copyright (c) 2015-2023 Advanced Micro Devices, Inc. All rights reserved. + * Copyright (c) 2015-2026 Advanced Micro Devices, Inc. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -23,6 +23,10 @@ */ #include #include +#include +#include +#include +#include TEST_CASE(interpolate_string_simple1) { @@ -124,4 +128,123 @@ TEST_CASE(slit_string_simple3) EXPECT(resuts.front() == "one two three"); } +TEST_CASE(to_hex_string_empty) +{ + const std::vector v{}; + EXPECT(migraphx::to_hex_string(v).empty()); + EXPECT(migraphx::to_hex_string(v, true).empty()); +} + +TEST_CASE(to_hex_string_uint8_single) +{ + const std::vector v{0xab}; + EXPECT(migraphx::to_hex_string(v) == "ab"); + // Single-byte elements are orientation-invariant. + EXPECT(migraphx::to_hex_string(v, true) == "ab"); +} + +TEST_CASE(to_hex_string_uint8_multiple) +{ + const std::vector v{0xca, 0xfe, 0xba, 0xbe}; + EXPECT(migraphx::to_hex_string(v) == "cafebabe"); + EXPECT(migraphx::to_hex_string(v, true) == "cafebabe"); +} + +TEST_CASE(to_hex_string_uint8_zero_and_max) +{ + const std::vector v{0x00, 0xff, 0x01, 0x10}; + EXPECT(migraphx::to_hex_string(v) == "00ff0110"); +} + +TEST_CASE(to_hex_string_uint16_msb) +{ + const std::vector v{0xabcd}; + EXPECT(migraphx::to_hex_string(v) == "abcd"); +} + +TEST_CASE(to_hex_string_uint16_lsb) +{ + const std::vector v{0xabcd}; + EXPECT(migraphx::to_hex_string(v, true) == "cdab"); +} + +TEST_CASE(to_hex_string_uint32_msb) +{ + const std::vector v{0xdeadbeef}; + EXPECT(migraphx::to_hex_string(v) == "deadbeef"); +} + +TEST_CASE(to_hex_string_uint32_lsb) +{ + const std::vector v{0xdeadbeef}; + EXPECT(migraphx::to_hex_string(v, true) == "efbeadde"); +} + +TEST_CASE(to_hex_string_uint64_msb) +{ + const std::vector v{0x0123456789abcdefULL}; + EXPECT(migraphx::to_hex_string(v) == "0123456789abcdef"); +} + +TEST_CASE(to_hex_string_uint64_lsb) +{ + const std::vector v{0x0123456789abcdefULL}; + EXPECT(migraphx::to_hex_string(v, true) == "efcdab8967452301"); +} + +TEST_CASE(to_hex_string_uint32_multiple) +{ + const std::vector v{0xdeadbeef, 0xcafebabe}; + EXPECT(migraphx::to_hex_string(v) == "deadbeefcafebabe"); + EXPECT(migraphx::to_hex_string(v, true) == "efbeaddebebafeca"); +} + +TEST_CASE(to_hex_string_zero_padding) +{ + // Each element produces exactly 2 * sizeof(T) characters, so small values + // are zero-padded to the full element width. + const std::vector v{0x00000001}; + EXPECT(migraphx::to_hex_string(v) == "00000001"); + EXPECT(migraphx::to_hex_string(v, true) == "01000000"); +} + +TEST_CASE(to_hex_string_std_array) +{ + const std::array a = {0x1234, 0x5678}; + EXPECT(migraphx::to_hex_string(a) == "12345678"); + EXPECT(migraphx::to_hex_string(a, true) == "34127856"); +} + +TEST_CASE(to_hex_string_initializer_list) +{ + EXPECT(migraphx::to_hex_string(std::initializer_list{0xde, 0xad}) == "dead"); +} + +TEST_CASE(to_hex_string_length) +{ + // Output length is exactly 2 * sizeof(T) * n regardless of element values. + const std::vector v(5, 0); + EXPECT(migraphx::to_hex_string(v).size() == 2 * sizeof(std::uint32_t) * v.size()); +} + +TEST_CASE(to_hex_string_signed_is_unsigned_bitpattern) +{ + // Signed inputs are reinterpreted through std::make_unsigned, so -1 prints + // as the all-ones byte pattern of its underlying width. + const std::vector v{std::int8_t{-1}}; + EXPECT(migraphx::to_hex_string(v) == "ff"); + + const std::vector w{std::int32_t{-1}}; + EXPECT(migraphx::to_hex_string(w) == "ffffffff"); + EXPECT(migraphx::to_hex_string(w, true) == "ffffffff"); +} + +TEST_CASE(to_hex_string_md5_initial_state_lsb) +{ + // LSB ordering matches MD5's canonical byte layout: the initial state + // serialized LSB-first is the well-known digest of the empty string. + const std::array state = {0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476}; + EXPECT(migraphx::to_hex_string(state, true) == "0123456789abcdeffedcba9876543210"); +} + int main(int argc, const char* argv[]) { test::run(argc, argv); }