Validate that SPIR-V binary is encoded as little endian for WebGPU (#2523)

Fixes #2522
diff --git a/source/val/validate.cpp b/source/val/validate.cpp
index b145b19..52009fc 100644
--- a/source/val/validate.cpp
+++ b/source/val/validate.cpp
@@ -281,6 +281,12 @@
            << "Invalid SPIR-V magic number.";
   }
 
+  if (spvIsWebGPUEnv(context.target_env) && endian != SPV_ENDIANNESS_LITTLE) {
+    return DiagnosticStream(position, context.consumer, "",
+                            SPV_ERROR_INVALID_BINARY)
+           << "WebGPU requires SPIR-V to be little endian.";
+  }
+
   spv_header_t header;
   if (spvBinaryHeaderGet(binary.get(), endian, &header)) {
     return DiagnosticStream(position, context.consumer, "",
diff --git a/test/val/val_webgpu_test.cpp b/test/val/val_webgpu_test.cpp
index 48ea21d..c55f927 100644
--- a/test/val/val_webgpu_test.cpp
+++ b/test/val/val_webgpu_test.cpp
@@ -276,6 +276,63 @@
                         "\"SPV_KHR_8bit_storage\"\n"));
 }
 
+spv_binary GenerateTrivialBinary(bool need_little_endian) {
+  // Smallest possible valid WebGPU SPIR-V binary in little endian. Contains all
+  // the required boilerplate and a trivial entry point function.
+  static const uint8_t binary_bytes[] = {
+      // clang-format off
+    0x03, 0x02, 0x23, 0x07, 0x00, 0x03, 0x01, 0x00, 0x00, 0x00, 0x07, 0x00,
+    0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x02, 0x00,
+    0x01, 0x00, 0x00, 0x00, 0x11, 0x00, 0x02, 0x00, 0xE1, 0x14, 0x00, 0x00,
+    0x0A, 0x00, 0x08, 0x00, 0x53, 0x50, 0x56, 0x5F, 0x4B, 0x48, 0x52, 0x5F,
+    0x76, 0x75, 0x6C, 0x6B, 0x61, 0x6E, 0x5F, 0x6D, 0x65, 0x6D, 0x6F, 0x72,
+    0x79, 0x5F, 0x6D, 0x6F, 0x64, 0x65, 0x6C, 0x00, 0x0E, 0x00, 0x03, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x05, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x73, 0x68, 0x61, 0x64,
+    0x65, 0x72, 0x00, 0x00, 0x13, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00,
+    0x21, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
+    0x36, 0x00, 0x05, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x02, 0x00,
+    0x04, 0x00, 0x00, 0x00, 0xFD, 0x00, 0x01, 0x00, 0x38, 0x00, 0x01, 0x00
+      // clang-format on
+  };
+  static const size_t word_count = sizeof(binary_bytes) / sizeof(uint32_t);
+  std::unique_ptr<spv_binary_t> result(new spv_binary_t);
+  if (!result) return nullptr;
+
+  result->wordCount = word_count;
+  result->code = new uint32_t[word_count];
+  if (!result->code) return nullptr;
+
+  if (need_little_endian) {
+    memcpy(result->code, binary_bytes, sizeof(binary_bytes));
+  } else {
+    uint8_t* code_bytes = reinterpret_cast<uint8_t*>(result->code);
+    for (size_t word = 0; word < word_count; ++word) {
+      code_bytes[4 * word] = binary_bytes[4 * word + 3];
+      code_bytes[4 * word + 1] = binary_bytes[4 * word + 2];
+      code_bytes[4 * word + 2] = binary_bytes[4 * word + 1];
+      code_bytes[4 * word + 3] = binary_bytes[4 * word];
+    }
+  }
+
+  return result.release();
+}
+
+TEST_F(ValidateWebGPU, LittleEndianGood) {
+  DestroyBinary();
+  binary_ = GenerateTrivialBinary(true);
+  EXPECT_EQ(SPV_SUCCESS, ValidateInstructions(SPV_ENV_WEBGPU_0));
+}
+
+TEST_F(ValidateWebGPU, BigEndianBad) {
+  DestroyBinary();
+  binary_ = GenerateTrivialBinary(false);
+  EXPECT_EQ(SPV_ERROR_INVALID_BINARY, ValidateInstructions(SPV_ENV_WEBGPU_0));
+  EXPECT_THAT(getDiagnosticString(),
+              HasSubstr("WebGPU requires SPIR-V to be little endian."));
+}
+
 }  // namespace
 }  // namespace val
 }  // namespace spvtools