diff --git a/.github/workflows/alpine.yml b/.github/workflows/alpine.yml index 3fee540..1322631 100644 --- a/.github/workflows/alpine.yml +++ b/.github/workflows/alpine.yml @@ -8,6 +8,9 @@ jobs: name: Build on Alpine ${{ matrix.arch }} runs-on: ubuntu-latest strategy: + # setup-alpine + qemu can flake on one arch (apk DB under emulation); + # do not cancel the others. + fail-fast: false matrix: arch: - x86_64 @@ -22,11 +25,15 @@ jobs: - name: Install extra dependencies run: | sudo apt update && sudo apt install -y binfmt-support - - name: Install latest Alpine Linux for ${{ matrix.arch }} + - name: Install Alpine Linux for ${{ matrix.arch }} uses: jirutka/setup-alpine@v1 with: arch: ${{ matrix.arch }} - branch: ${{ matrix.arch == 'riscv64' && 'edge' || 'latest-stable' }} + # latest-stable is 3.23+ (apk-tools v3). setup-alpine still + # bootstraps with a static apk v2.14 binary, so apk add in the + # chroot fails with "database: file format is invalid" on some + # qemu arches (seen on ppc64le). v3.22 is the last apk v2 release. + branch: ${{ matrix.arch == 'riscv64' && 'edge' || 'v3.22' }} packages: > build-base cmake diff --git a/include/ada/idna/to_ascii.h b/include/ada/idna/to_ascii.h index 2eb46d5..d1acf81 100644 --- a/include/ada/idna/to_ascii.h +++ b/include/ada/idna/to_ascii.h @@ -28,8 +28,10 @@ std::string to_ascii(std::string_view ut8_string); // https://url.spec.whatwg.org/#forbidden-domain-code-point bool contains_forbidden_domain_code_point(std::string_view ascii_string); -bool constexpr is_ascii(std::u32string_view view); -bool constexpr is_ascii(std::string_view view); +// Runtime SIMD (SSE2/NEON) with a SWAR fallback. Not constexpr: vector +// paths cannot be evaluated at compile time. +bool is_ascii(std::u32string_view view) noexcept; +bool is_ascii(std::string_view view) noexcept; } // namespace ada::idna diff --git a/src/mapping.cpp b/src/mapping.cpp index f72f215..9028860 100644 --- a/src/mapping.cpp +++ b/src/mapping.cpp @@ -6,6 +6,7 @@ #include #include "table_store.hpp" +#include "simd.hpp" #include "mapping_tables.cpp" namespace ada::idna { @@ -81,28 +82,7 @@ static size_t utf8_count_codepoints(const uint8_t* ptr) noexcept { // --- ASCII fast path --------------------------------------------------------- void ascii_map(char* input, size_t length) { - auto broadcast = [](uint8_t v) -> uint64_t { - return 0x101010101010101ull * v; - }; - uint64_t broadcast_80 = broadcast(0x80); - uint64_t broadcast_Ap = broadcast(128 - 'A'); - uint64_t broadcast_Zp = broadcast(128 - 'Z' - 1); - size_t i = 0; - - for (; i + 7 < length; i += 8) { - uint64_t word{}; - std::memcpy(&word, input + i, sizeof(word)); - word ^= - (((word + broadcast_Ap) ^ (word + broadcast_Zp)) & broadcast_80) >> 2; - std::memcpy(input + i, &word, sizeof(word)); - } - if (i < length) { - uint64_t word{}; - std::memcpy(&word, input + i, length - i); - word ^= - (((word + broadcast_Ap) ^ (word + broadcast_Zp)) & broadcast_80) >> 2; - std::memcpy(input + i, &word, length - i); - } + (void)simd::ascii_lowercase_is_ascii(input, length); } // Two-pass map: first validate + exact size, then write once (no growth diff --git a/src/simd.hpp b/src/simd.hpp new file mode 100644 index 0000000..d5afb5c --- /dev/null +++ b/src/simd.hpp @@ -0,0 +1,382 @@ +#ifndef ADA_IDNA_SIMD_HPP +#define ADA_IDNA_SIMD_HPP + +// Portable SIMD helpers for IDNA hot paths. SSE2 is baseline on x86_64; +// NEON is baseline on aarch64. Other targets use SWAR. + +#include +#include +#include + +#if defined(__SSE2__) || defined(__x86_64__) || defined(__x86_64) || \ + defined(_M_AMD64) || defined(_M_X64) || \ + (defined(_M_IX86_FP) && _M_IX86_FP == 2) +#define ADA_IDNA_SSE2 1 +#include +#endif + +#if defined(__aarch64__) || defined(_M_ARM64) +#define ADA_IDNA_NEON 1 +#include +#endif + +#if defined(_MSC_VER) && !defined(__clang__) +#define ADA_IDNA_REALLY_INLINE __forceinline +#else +#define ADA_IDNA_REALLY_INLINE inline __attribute__((always_inline)) +#endif + +#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86)) +#include +#endif + +namespace ada::idna::simd { + +// Byte-parallel ASCII A-Z -> a-z (Hacker's Delight / aqrit). +static constexpr uint64_t k80 = 0x8080808080808080ull; +static constexpr uint64_t kAp = 0x3F3F3F3F3F3F3F3Full; // 128 - 'A' +static constexpr uint64_t kZp = 0x2525252525252525ull; // 128 - 'Z' - 1 + +ADA_IDNA_REALLY_INLINE uint64_t lower8(uint64_t word) noexcept { + return word ^ ((((word + kAp) ^ (word + kZp)) & k80) >> 2); +} + +#if defined(ADA_IDNA_SSE2) +ADA_IDNA_REALLY_INLINE __m128i lower16(__m128i word) noexcept { + const __m128i v80 = _mm_set1_epi8(static_cast(0x80)); + const __m128i vAp = _mm_set1_epi8(static_cast(128 - 'A')); + const __m128i vZp = _mm_set1_epi8(static_cast(128 - 'Z' - 1)); + const __m128i mask = _mm_and_si128( + _mm_xor_si128(_mm_add_epi8(word, vAp), _mm_add_epi8(word, vZp)), v80); + // mask is 0x00/0x80 per byte; >>2 -> 0x00/0x20 without crossing lanes. + return _mm_xor_si128(word, _mm_srli_epi16(mask, 2)); +} + +ADA_IDNA_REALLY_INLINE void widen16(__m128i bytes, char32_t* dst) noexcept { + const __m128i zero = _mm_setzero_si128(); + const __m128i lo16 = _mm_unpacklo_epi8(bytes, zero); + const __m128i hi16 = _mm_unpackhi_epi8(bytes, zero); + _mm_storeu_si128(reinterpret_cast<__m128i*>(dst), + _mm_unpacklo_epi16(lo16, zero)); + _mm_storeu_si128(reinterpret_cast<__m128i*>(dst + 4), + _mm_unpackhi_epi16(lo16, zero)); + _mm_storeu_si128(reinterpret_cast<__m128i*>(dst + 8), + _mm_unpacklo_epi16(hi16, zero)); + _mm_storeu_si128(reinterpret_cast<__m128i*>(dst + 12), + _mm_unpackhi_epi16(hi16, zero)); +} + +ADA_IDNA_REALLY_INLINE int popcount_u32(unsigned v) noexcept { +#if defined(_MSC_VER) + return static_cast(__popcnt(v)); +#else + return __builtin_popcount(v); +#endif +} +#endif + +#if defined(ADA_IDNA_NEON) +ADA_IDNA_REALLY_INLINE uint8x16_t lower16(uint8x16_t word) noexcept { + const uint8x16_t v80 = vdupq_n_u8(0x80); + const uint8x16_t vAp = vdupq_n_u8(static_cast(128 - 'A')); + const uint8x16_t vZp = vdupq_n_u8(static_cast(128 - 'Z' - 1)); + const uint8x16_t mask = + vandq_u8(veorq_u8(vaddq_u8(word, vAp), vaddq_u8(word, vZp)), v80); + return veorq_u8(word, vshrq_n_u8(mask, 2)); +} + +ADA_IDNA_REALLY_INLINE void widen16(uint8x16_t bytes, char32_t* dst) noexcept { + const uint16x8_t lo16 = vmovl_u8(vget_low_u8(bytes)); + const uint16x8_t hi16 = vmovl_u8(vget_high_u8(bytes)); + vst1q_u32(reinterpret_cast(dst), vmovl_u16(vget_low_u16(lo16))); + vst1q_u32(reinterpret_cast(dst + 4), + vmovl_u16(vget_high_u16(lo16))); + vst1q_u32(reinterpret_cast(dst + 8), + vmovl_u16(vget_low_u16(hi16))); + vst1q_u32(reinterpret_cast(dst + 12), + vmovl_u16(vget_high_u16(hi16))); +} +#endif + +// In-place A-Z -> a-z. Returns true iff every original byte was ASCII. +// One pass replaces a separate is_ascii scan plus ascii_map. +ADA_IDNA_REALLY_INLINE bool ascii_lowercase_is_ascii(char* input, + size_t length) noexcept { + bool ascii = true; + uint64_t high = 0; + size_t i = 0; +#if defined(ADA_IDNA_SSE2) + __m128i vacc = _mm_setzero_si128(); + for (; i + 32 <= length; i += 32) { + const __m128i w0 = + _mm_loadu_si128(reinterpret_cast(input + i)); + const __m128i w1 = + _mm_loadu_si128(reinterpret_cast(input + i + 16)); + vacc = _mm_or_si128(vacc, _mm_or_si128(w0, w1)); + _mm_storeu_si128(reinterpret_cast<__m128i*>(input + i), lower16(w0)); + _mm_storeu_si128(reinterpret_cast<__m128i*>(input + i + 16), lower16(w1)); + } + if (i + 16 <= length) { + const __m128i w = + _mm_loadu_si128(reinterpret_cast(input + i)); + vacc = _mm_or_si128(vacc, w); + _mm_storeu_si128(reinterpret_cast<__m128i*>(input + i), lower16(w)); + i += 16; + } + ascii = _mm_movemask_epi8(vacc) == 0; +#elif defined(ADA_IDNA_NEON) + uint8x16_t vacc = vdupq_n_u8(0); + for (; i + 32 <= length; i += 32) { + const uint8x16_t w0 = vld1q_u8(reinterpret_cast(input + i)); + const uint8x16_t w1 = + vld1q_u8(reinterpret_cast(input + i + 16)); + vacc = vorrq_u8(vacc, vorrq_u8(w0, w1)); + vst1q_u8(reinterpret_cast(input + i), lower16(w0)); + vst1q_u8(reinterpret_cast(input + i + 16), lower16(w1)); + } + if (i + 16 <= length) { + const uint8x16_t w = vld1q_u8(reinterpret_cast(input + i)); + vacc = vorrq_u8(vacc, w); + vst1q_u8(reinterpret_cast(input + i), lower16(w)); + i += 16; + } + ascii = vmaxvq_u8(vacc) < 0x80; +#endif + for (; i + 8 <= length; i += 8) { + uint64_t word = 0; + std::memcpy(&word, input + i, 8); + high |= word; + word = lower8(word); + std::memcpy(input + i, &word, 8); + } + if (i < length) { + uint64_t word = 0; + std::memcpy(&word, input + i, length - i); + high |= word; + word = lower8(word); + std::memcpy(input + i, &word, length - i); + } + return ascii && (high & k80) == 0; +} + +ADA_IDNA_REALLY_INLINE bool is_ascii_8(const char* data, size_t len) noexcept { + const uint8_t* p = reinterpret_cast(data); + uint64_t acc = 0; + size_t i = 0; +#if defined(ADA_IDNA_SSE2) + if (len >= 16) { + __m128i vacc = _mm_setzero_si128(); + for (; i + 32 <= len; i += 32) { + vacc = _mm_or_si128( + vacc, + _mm_or_si128( + _mm_loadu_si128(reinterpret_cast(p + i)), + _mm_loadu_si128(reinterpret_cast(p + i + 16)))); + } + if (i + 16 <= len) { + vacc = _mm_or_si128( + vacc, _mm_loadu_si128(reinterpret_cast(p + i))); + i += 16; + } + if (_mm_movemask_epi8(vacc) != 0) { + return false; + } + } +#elif defined(ADA_IDNA_NEON) + if (len >= 16) { + uint8x16_t vacc = vdupq_n_u8(0); + for (; i + 32 <= len; i += 32) { + vacc = vorrq_u8(vacc, vorrq_u8(vld1q_u8(p + i), vld1q_u8(p + i + 16))); + } + if (i + 16 <= len) { + vacc = vorrq_u8(vacc, vld1q_u8(p + i)); + i += 16; + } + if (vmaxvq_u8(vacc) >= 0x80) { + return false; + } + } +#endif + for (; i + 8 <= len; i += 8) { + uint64_t word = 0; + std::memcpy(&word, p + i, 8); + acc |= word; + } + if (i < len) { + uint64_t word = 0; + std::memcpy(&word, p + i, len - i); + acc |= word; + } + return (acc & k80) == 0; +} + +// SWAR over uint64 pairs: one load / two code points, no vector setup. +ADA_IDNA_REALLY_INLINE bool is_ascii_32(const char32_t* data, + size_t len) noexcept { + const uint32_t* p = reinterpret_cast(data); + uint64_t acc = 0; + size_t i = 0; + for (; i + 2 <= len; i += 2) { + uint64_t word = 0; + std::memcpy(&word, p + i, 8); + acc |= word; + } + if (i < len) { + acc |= p[i]; + } + return (acc & 0xFFFFFF80FFFFFF80ull) == 0; +} + +// Load 16 bytes once: if ASCII, widen from the same register. +ADA_IDNA_REALLY_INLINE bool try_widen16_ascii(const uint8_t* src, + char32_t* dst) noexcept { +#if defined(ADA_IDNA_SSE2) + const __m128i bytes = _mm_loadu_si128(reinterpret_cast(src)); + if (_mm_movemask_epi8(bytes) != 0) { + return false; + } + widen16(bytes, dst); + return true; +#elif defined(ADA_IDNA_NEON) + const uint8x16_t bytes = vld1q_u8(src); + if (vmaxvq_u8(bytes) >= 0x80) { + return false; + } + widen16(bytes, dst); + return true; +#else + uint64_t v1 = 0; + uint64_t v2 = 0; + std::memcpy(&v1, src, 8); + std::memcpy(&v2, src + 8, 8); + if (((v1 | v2) & k80) != 0) { + return false; + } + for (size_t k = 0; k < 16; ++k) { + dst[k] = static_cast(src[k]); + } + return true; +#endif +} + +// Load 4 UTF-32 values once: if ASCII, pack to 4 bytes. +ADA_IDNA_REALLY_INLINE bool try_pack4_ascii(const uint32_t* src, + char* dst) noexcept { +#if defined(ADA_IDNA_SSE2) + __m128i v = _mm_loadu_si128(reinterpret_cast(src)); + if (_mm_movemask_ps( + _mm_castsi128_ps(_mm_cmpgt_epi32(v, _mm_set1_epi32(0x7F)))) != 0) { + return false; + } + v = _mm_packs_epi32(v, v); + v = _mm_packus_epi16(v, v); + const uint32_t out = static_cast(_mm_cvtsi128_si32(v)); + std::memcpy(dst, &out, 4); + return true; +#elif defined(ADA_IDNA_NEON) + const uint32x4_t v = vld1q_u32(src); + if (vmaxvq_u32(v) >= 0x80u) { + return false; + } + const uint8x8_t n8 = vmovn_u16(vcombine_u16(vmovn_u32(v), vmovn_u32(v))); + vst1_lane_u32(reinterpret_cast(dst), vreinterpret_u32_u8(n8), 0); + return true; +#else + uint64_t w0 = 0; + uint64_t w1 = 0; + std::memcpy(&w0, src, 8); + std::memcpy(&w1, src + 2, 8); + if (((w0 | w1) & 0xFFFFFF80FFFFFF80ull) != 0) { + return false; + } + dst[0] = static_cast(src[0]); + dst[1] = static_cast(src[1]); + dst[2] = static_cast(src[2]); + dst[3] = static_cast(src[3]); + return true; +#endif +} + +ADA_IDNA_REALLY_INLINE size_t utf32_length_from_utf8(const char* buf, + size_t len) noexcept { + const int8_t* p = reinterpret_cast(buf); + size_t count = 0; + size_t i = 0; +#if defined(ADA_IDNA_SSE2) + const __m128i thresh = _mm_set1_epi8(static_cast(-65)); + for (; i + 32 <= len; i += 32) { + const unsigned m0 = static_cast(_mm_movemask_epi8(_mm_cmpgt_epi8( + _mm_loadu_si128(reinterpret_cast(p + i)), thresh))); + const unsigned m1 = static_cast(_mm_movemask_epi8(_mm_cmpgt_epi8( + _mm_loadu_si128(reinterpret_cast(p + i + 16)), + thresh))); + count += static_cast(popcount_u32(m0) + popcount_u32(m1)); + } + for (; i + 16 <= len; i += 16) { + const unsigned mask = + static_cast(_mm_movemask_epi8(_mm_cmpgt_epi8( + _mm_loadu_si128(reinterpret_cast(p + i)), thresh))); + count += static_cast(popcount_u32(mask)); + } +#elif defined(ADA_IDNA_NEON) + // vcgtq_s8 already returns uint8x16_t (ACLE). Do not vreinterpret. + const int8x16_t thresh = vdupq_n_s8(static_cast(-65)); + const uint8x16_t one = vdupq_n_u8(1); + for (; i + 16 <= len; i += 16) { + const uint8x16_t gt = vcgtq_s8(vld1q_s8(p + i), thresh); + count += static_cast(vaddvq_u8(vandq_u8(gt, one))); + } +#endif + for (; i < len; ++i) { + count += static_cast(p[i] > static_cast(-65)); + } + return count; +} + +ADA_IDNA_REALLY_INLINE size_t utf8_length_from_utf32(const char32_t* buf, + size_t len) noexcept { + const uint32_t* p = reinterpret_cast(buf); + size_t count = 0; + size_t i = 0; +#if defined(ADA_IDNA_SSE2) + const __m128i ones = _mm_set1_epi32(1); + const __m128i lim7f = _mm_set1_epi32(0x7F); + const __m128i lim7ff = _mm_set1_epi32(0x7FF); + const __m128i limffff = _mm_set1_epi32(0xFFFF); + __m128i acc = _mm_setzero_si128(); + for (; i + 4 <= len; i += 4) { + const __m128i v = _mm_loadu_si128(reinterpret_cast(p + i)); + __m128i c = ones; + c = _mm_sub_epi32(c, _mm_cmpgt_epi32(v, lim7f)); + c = _mm_sub_epi32(c, _mm_cmpgt_epi32(v, lim7ff)); + c = _mm_sub_epi32(c, _mm_cmpgt_epi32(v, limffff)); + acc = _mm_add_epi32(acc, c); + } + acc = _mm_add_epi32(acc, _mm_shuffle_epi32(acc, 0x4E)); + acc = _mm_add_epi32(acc, _mm_shuffle_epi32(acc, 0xB1)); + count = static_cast(static_cast(_mm_cvtsi128_si32(acc))); +#elif defined(ADA_IDNA_NEON) + uint32x4_t acc = vdupq_n_u32(0); + const uint32x4_t one = vdupq_n_u32(1); + for (; i + 4 <= len; i += 4) { + const uint32x4_t v = vld1q_u32(p + i); + uint32x4_t c = one; + c = vaddq_u32(c, vandq_u32(vcgtq_u32(v, vdupq_n_u32(0x7F)), one)); + c = vaddq_u32(c, vandq_u32(vcgtq_u32(v, vdupq_n_u32(0x7FF)), one)); + c = vaddq_u32(c, vandq_u32(vcgtq_u32(v, vdupq_n_u32(0xFFFF)), one)); + acc = vaddq_u32(acc, c); + } + count = static_cast(vaddvq_u32(acc)); +#endif + for (; i < len; ++i) { + ++count; + count += static_cast(p[i] > 0x7Fu); + count += static_cast(p[i] > 0x7FFu); + count += static_cast(p[i] > 0xFFFFu); + } + return count; +} + +} // namespace ada::idna::simd + +#endif // ADA_IDNA_SIMD_HPP diff --git a/src/to_ascii.cpp b/src/to_ascii.cpp index 4b7a3b3..411d014 100644 --- a/src/to_ascii.cpp +++ b/src/to_ascii.cpp @@ -1,15 +1,13 @@ #include "ada/idna/to_ascii.h" -#include #include -#include -#include #include "ada/idna/mapping.h" #include "ada/idna/normalization.h" #include "ada/idna/punycode.h" #include "ada/idna/unicode_transcoding.h" #include "ada/idna/validity.h" +#include "simd.hpp" #ifdef ADA_USE_SIMDUTF #include "simdutf.h" @@ -17,22 +15,12 @@ namespace ada::idna { -bool constexpr is_ascii(std::u32string_view view) { - for (uint32_t c : view) { - if (c >= 0x80) { - return false; - } - } - return true; +bool is_ascii(std::u32string_view view) noexcept { + return simd::is_ascii_32(view.data(), view.size()); } -bool constexpr is_ascii(std::string_view view) { - for (uint8_t c : view) { - if (c >= 0x80) { - return false; - } - } - return true; +bool is_ascii(std::string_view view) noexcept { + return simd::is_ascii_8(view.data(), view.size()); } constexpr static uint8_t is_forbidden_domain_code_point_table[] = { @@ -50,22 +38,23 @@ constexpr static uint8_t is_forbidden_domain_code_point_table[] = { static_assert(sizeof(is_forbidden_domain_code_point_table) == 256); -inline bool is_forbidden_domain_code_point(const char c) noexcept { - return is_forbidden_domain_code_point_table[uint8_t(c)]; -} - bool contains_forbidden_domain_code_point(std::string_view view) { - return std::ranges::any_of(view, is_forbidden_domain_code_point); -} - -// Per the WHATWG URL "domain to ASCII" algorithm, when beStrict is false and -// the input domain is an ASCII string, the result is the input lowercased, -// regardless of the outcome of Unicode ToASCII. -// -// See https://url.spec.whatwg.org/#concept-domain-to-ascii -static void from_ascii_to_ascii(std::string_view ut8_string, std::string& out) { - out.assign(ut8_string); - ascii_map(out.data(), out.size()); + const auto* p = reinterpret_cast(view.data()); + const size_t n = view.size(); + size_t i = 0; + uint8_t bits = 0; + for (; i + 4 <= n; i += 4) { + bits = + static_cast(bits | is_forbidden_domain_code_point_table[p[i]] | + is_forbidden_domain_code_point_table[p[i + 1]] | + is_forbidden_domain_code_point_table[p[i + 2]] | + is_forbidden_domain_code_point_table[p[i + 3]]); + } + for (; i < n; ++i) { + bits = + static_cast(bits | is_forbidden_domain_code_point_table[p[i]]); + } + return bits != 0; } // Append ASCII code units from a UTF-32 label (all values < 0x80). @@ -73,8 +62,9 @@ static void append_ascii_label(std::string& out, std::u32string_view label) { const size_t old = out.size(); out.resize(old + label.size()); char* dest = out.data() + old; - for (char32_t c : label) { - *dest++ = static_cast(c); + const char32_t* src = label.data(); + for (size_t i = 0; i < label.size(); ++i) { + dest[i] = static_cast(src[i]); } } @@ -85,14 +75,18 @@ static bool is_ace_prefix(std::u32string_view label) noexcept { } [[nodiscard]] bool to_ascii(std::string_view ut8_string, std::string& out) { - out.clear(); if (ut8_string.size() > max_domain_input_bytes) { + out.clear(); return false; } - if (is_ascii(ut8_string)) { - from_ascii_to_ascii(ut8_string, out); + // WHATWG beStrict=false: ASCII input is just copied and lowercased. + // One pass: lowercase in place and test the high bit (no extra is_ascii + // scan). See https://url.spec.whatwg.org/#concept-domain-to-ascii + out.assign(ut8_string); + if (simd::ascii_lowercase_is_ascii(out.data(), out.size())) { return true; } + out.clear(); #ifdef ADA_USE_SIMDUTF size_t utf32_length = @@ -156,11 +150,9 @@ static bool is_ace_prefix(std::u32string_view label) noexcept { if (label_size == 0) { // empty label } else if (is_ace_prefix(label_view)) { - for (char32_t c : label_view) { - if (c >= 0x80) { - out.clear(); - return false; - } + if (!is_ascii(label_view)) { + out.clear(); + return false; } append_ascii_label(out, label_view); std::string_view puny_segment_ascii( diff --git a/src/unicode_transcoding.cpp b/src/unicode_transcoding.cpp index bc2708c..ac8f0a9 100644 --- a/src/unicode_transcoding.cpp +++ b/src/unicode_transcoding.cpp @@ -1,8 +1,8 @@ #include "ada/idna/unicode_transcoding.h" -#include #include -#include + +#include "simd.hpp" namespace ada::idna { @@ -11,22 +11,11 @@ size_t utf8_to_utf32(const char* buf, size_t len, char32_t* utf32_output) { size_t pos = 0; const char32_t* start{utf32_output}; while (pos < len) { - // try to convert the next block of 16 ASCII bytes - if (pos + 16 <= len) { // if it is safe to read 16 more - // bytes, check that they are ascii - uint64_t v1; - std::memcpy(&v1, data + pos, sizeof(uint64_t)); - uint64_t v2; - std::memcpy(&v2, data + pos + sizeof(uint64_t), sizeof(uint64_t)); - uint64_t v{v1 | v2}; - if ((v & 0x8080808080808080) == 0) { - size_t final_pos = pos + 16; - while (pos < final_pos) { - *utf32_output++ = char32_t(buf[pos]); - pos++; - } - continue; - } + // One load: ASCII check and widen share the same 16-byte register. + if (pos + 16 <= len && simd::try_widen16_ascii(data + pos, utf32_output)) { + utf32_output += 16; + pos += 16; + continue; } uint8_t leading_byte = data[pos]; // leading byte if (leading_byte < 0b10000000) { @@ -105,24 +94,13 @@ size_t utf8_to_utf32(const char* buf, size_t len, char32_t* utf32_output) { size_t utf8_length_from_utf32(const char32_t* buf, size_t len) { // We are not BOM aware. - const uint32_t* p = reinterpret_cast(buf); - size_t counter{0}; - for (size_t i = 0; i != len; ++i) { - ++counter; // ASCII - counter += static_cast(p[i] > 0x7F); // two-byte - counter += static_cast(p[i] > 0x7FF); // three-byte - counter += static_cast(p[i] > 0xFFFF); // four-bytes - } - return counter; + return simd::utf8_length_from_utf32(buf, len); } size_t utf32_length_from_utf8(const char* buf, size_t len) { - const int8_t* p = reinterpret_cast(buf); - return std::count_if(p, std::next(p, len), [](int8_t c) { - // -65 is 0b10111111, anything larger in two-complement's - // should start a new code point. - return c > -65; - }); + // -65 is 0b10111111; anything larger in two's complement starts a + // new code point (not a UTF-8 continuation byte). + return simd::utf32_length_from_utf8(buf, len); } size_t utf32_to_utf8(const char32_t* buf, size_t len, char* utf8_output) { @@ -130,17 +108,10 @@ size_t utf32_to_utf8(const char32_t* buf, size_t len, char* utf8_output) { size_t pos = 0; const char* start{utf8_output}; while (pos < len) { - // try to convert the next block of 2 ASCII characters - if (pos + 2 <= len) { // if it is safe to read 8 more - // bytes, check that they are ascii - uint64_t v; - std::memcpy(&v, data + pos, sizeof(uint64_t)); - if ((v & 0xFFFFFF80FFFFFF80) == 0) { - *utf8_output++ = char(buf[pos]); - *utf8_output++ = char(buf[pos + 1]); - pos += 2; - continue; - } + if (pos + 4 <= len && simd::try_pack4_ascii(data + pos, utf8_output)) { + utf8_output += 4; + pos += 4; + continue; } uint32_t word = data[pos]; if ((word & 0xFFFFFF80) == 0) { diff --git a/tests/mapping_tests.cpp b/tests/mapping_tests.cpp index 6fc896f..c38840b 100644 --- a/tests/mapping_tests.cpp +++ b/tests/mapping_tests.cpp @@ -254,6 +254,30 @@ TEST(mapping_tests, string_multi_cp_mappings_in_context) { TEST(mapping_tests, empty_string) { EXPECT_EQ(ada::idna::map(U""), U""); } +TEST(mapping_tests, ascii_map_simd_widths) { + auto lower = [](std::string s) { + ada::idna::ascii_map(s.data(), s.size()); + return s; + }; + EXPECT_EQ(lower(""), ""); + EXPECT_EQ(lower("A"), "a"); + EXPECT_EQ(lower("Z"), "z"); + EXPECT_EQ(lower("a"), "a"); + EXPECT_EQ(lower("0123-._~"), "0123-._~"); + EXPECT_EQ(lower("ABCDEFg"), "abcdefg"); // 7 + EXPECT_EQ(lower("ABCDEFGH"), "abcdefgh"); // 8 + EXPECT_EQ(lower("ABCDEFGHIJKLMNO"), "abcdefghijklmno"); // 15 + EXPECT_EQ(lower("ABCDEFGHIJKLMNOP"), "abcdefghijklmnop"); // 16 + EXPECT_EQ(lower("ABCDEFGHIJKLMNOPQ"), "abcdefghijklmnopq"); // 17 + EXPECT_EQ(lower(std::string(32, 'A')), std::string(32, 'a')); + EXPECT_EQ(lower(std::string(33, 'Z')), std::string(33, 'z')); + EXPECT_EQ(lower("Example.COM"), "example.com"); + // Unaligned destination (offset 1 inside a larger buffer). + std::string padded = std::string(1, '!') + std::string(20, 'B'); + ada::idna::ascii_map(padded.data() + 1, 20); + EXPECT_EQ(padded, std::string(1, '!') + std::string(20, 'b')); +} + // ── CJK compatibility ideographs in the mapping range ───────────────────── // These are in the high area of the two-level table (0x2F800 range). TEST(mapping_tests, cjk_compatibility_ideographs) { diff --git a/tests/safety_tests.cpp b/tests/safety_tests.cpp index ec64751..ee19508 100644 --- a/tests/safety_tests.cpp +++ b/tests/safety_tests.cpp @@ -101,3 +101,120 @@ TEST(Safety, IsAlreadyNfc) { EXPECT_TRUE(ada::idna::is_already_nfc(precomposed)); EXPECT_FALSE(ada::idna::is_already_nfc(decomposed)); } + +TEST(Safety, IsAsciiUtf8LengthsAndAlignment) { + EXPECT_TRUE(ada::idna::is_ascii(std::string_view{})); + EXPECT_TRUE(ada::idna::is_ascii("")); + EXPECT_TRUE(ada::idna::is_ascii("a")); + EXPECT_TRUE(ada::idna::is_ascii("example.com")); + // 15 / 16 / 17 / 32 / 33 cover SWAR tail, SIMD block, and overlap. + const std::string a15(15, 'a'); + const std::string a16(16, 'b'); + const std::string a17(17, 'c'); + const std::string a32(32, 'd'); + const std::string a33(33, 'e'); + EXPECT_TRUE(ada::idna::is_ascii(a15)); + EXPECT_TRUE(ada::idna::is_ascii(a16)); + EXPECT_TRUE(ada::idna::is_ascii(a17)); + EXPECT_TRUE(ada::idna::is_ascii(a32)); + EXPECT_TRUE(ada::idna::is_ascii(a33)); + + std::string high_first = a16; + high_first[0] = static_cast(0x80); + EXPECT_FALSE(ada::idna::is_ascii(high_first)); + + std::string high_last16 = a16; + high_last16[15] = static_cast(0xFF); + EXPECT_FALSE(ada::idna::is_ascii(high_last16)); + + std::string high_mid32 = a32; + high_mid32[16] = static_cast(0xC3); + EXPECT_FALSE(ada::idna::is_ascii(high_mid32)); + + std::string high_tail = a33; + high_tail[32] = static_cast(0x80); + EXPECT_FALSE(ada::idna::is_ascii(high_tail)); + + // Unaligned view (offset 1) still uses unaligned SIMD loads. + std::string padded = std::string(1, 'z') + a32; + EXPECT_TRUE(ada::idna::is_ascii(std::string_view(padded).substr(1))); + padded[1 + 20] = static_cast(0x80); + EXPECT_FALSE(ada::idna::is_ascii(std::string_view(padded).substr(1))); +} + +TEST(Safety, IsAsciiUtf32Lengths) { + EXPECT_TRUE(ada::idna::is_ascii(std::u32string_view{})); + EXPECT_TRUE(ada::idna::is_ascii(std::u32string_view(U"abc"))); + EXPECT_TRUE(ada::idna::is_ascii(std::u32string(3, U'x'))); + EXPECT_TRUE(ada::idna::is_ascii(std::u32string(4, U'x'))); + EXPECT_TRUE(ada::idna::is_ascii(std::u32string(5, U'x'))); + EXPECT_TRUE(ada::idna::is_ascii(std::u32string(8, U'x'))); + EXPECT_TRUE(ada::idna::is_ascii(std::u32string(9, U'x'))); + + std::u32string mixed(8, U'a'); + mixed[0] = 0xE9; + EXPECT_FALSE(ada::idna::is_ascii(std::u32string_view(mixed))); + mixed[0] = U'a'; + mixed[3] = 0x80; + EXPECT_FALSE(ada::idna::is_ascii(std::u32string_view(mixed))); + mixed[3] = U'a'; + mixed[7] = 0x10FFFF; + EXPECT_FALSE(ada::idna::is_ascii(std::u32string_view(mixed))); +} + +TEST(Safety, ForbiddenDomainCodePointsSimdWidths) { + EXPECT_FALSE(ada::idna::contains_forbidden_domain_code_point("example.com")); + EXPECT_FALSE(ada::idna::contains_forbidden_domain_code_point("")); + const std::string ok16(16, 'a'); + const std::string ok32(32, 'b'); + EXPECT_FALSE(ada::idna::contains_forbidden_domain_code_point(ok16)); + EXPECT_FALSE(ada::idna::contains_forbidden_domain_code_point(ok32)); + + EXPECT_TRUE(ada::idna::contains_forbidden_domain_code_point("foo bar")); + EXPECT_TRUE(ada::idna::contains_forbidden_domain_code_point("foo#bar")); + EXPECT_TRUE(ada::idna::contains_forbidden_domain_code_point("foo/bar")); + EXPECT_TRUE(ada::idna::contains_forbidden_domain_code_point("foo:bar")); + EXPECT_TRUE(ada::idna::contains_forbidden_domain_code_point("foo?bar")); + EXPECT_TRUE(ada::idna::contains_forbidden_domain_code_point("foo@bar")); + + std::string high = ok16; + high[10] = static_cast(0x80); + EXPECT_TRUE(ada::idna::contains_forbidden_domain_code_point(high)); + + std::string hash_at_16 = ok16 + "#tail"; + EXPECT_TRUE(ada::idna::contains_forbidden_domain_code_point(hash_at_16)); +} + +TEST(Safety, UtfTranscodingAsciiBlocks) { + const std::string ascii32(32, 'A'); + const size_t n32 = + ada::idna::utf32_length_from_utf8(ascii32.data(), ascii32.size()); + EXPECT_EQ(n32, 32u); + std::u32string u32(n32, U'\0'); + EXPECT_EQ( + ada::idna::utf8_to_utf32(ascii32.data(), ascii32.size(), u32.data()), + 32u); + EXPECT_EQ(u32, std::u32string(32, U'A')); + EXPECT_EQ(ada::idna::utf8_length_from_utf32(u32.data(), u32.size()), 32u); + std::string back(32, '\0'); + EXPECT_EQ(ada::idna::utf32_to_utf8(u32.data(), u32.size(), back.data()), 32u); + EXPECT_EQ(back, ascii32); + + // Mixed: 16 ASCII + 2-byte UTF-8 + 16 ASCII (hits SIMD then scalar). + std::string mixed = std::string(16, 'x') + "\xc3\xa9" + std::string(16, 'y'); + const size_t n_mixed = + ada::idna::utf32_length_from_utf8(mixed.data(), mixed.size()); + EXPECT_EQ(n_mixed, 33u); + std::u32string u_mixed(n_mixed, U'\0'); + EXPECT_EQ( + ada::idna::utf8_to_utf32(mixed.data(), mixed.size(), u_mixed.data()), + 33u); + EXPECT_EQ(u_mixed[16], char32_t(0xE9)); + EXPECT_EQ(ada::idna::utf8_length_from_utf32(u_mixed.data(), u_mixed.size()), + mixed.size()); + std::string mixed_back(mixed.size(), '\0'); + EXPECT_EQ(ada::idna::utf32_to_utf8(u_mixed.data(), u_mixed.size(), + mixed_back.data()), + mixed.size()); + EXPECT_EQ(mixed_back, mixed); +}