This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch cassandra-5.0
in repository https://gitbox.apache.org/repos/asf/cassandra.git

commit d020df9ce3c48c709fedf8f1400996d3f194beac
Merge: 06468fb06b 2bebf8f0b8
Author: Marcus Eriksson <[email protected]>
AuthorDate: Wed Oct 21 15:28:47 2026 +0200

    Merge branch 'cassandra-4.1' into cassandra-5.0

 CHANGES.txt                                        |   1 +
 conf/cassandra.yaml                                |   8 ++
 src/java/org/apache/cassandra/config/Config.java   |   5 +
 .../cassandra/config/DatabaseDescriptor.java       |  10 ++
 .../CassandraEntireSSTableStreamReader.java        |  39 ++++++
 .../cassandra/io/util/DataIntegrityMetadata.java   |  10 +-
 .../apache/cassandra/metrics/StreamingMetrics.java |   8 ++
 .../apache/cassandra/streaming/StreamSession.java  |   5 +
 .../test/hostreplacement/HostReplacementTest.java  |  83 +++++++++++++
 .../CassandraEntireSSTableStreamWriterTest.java    | 135 +++++++++++++++++++++
 .../io/util/DataIntegrityMetadataTest.java         |  72 +++++++++++
 11 files changed, 375 insertions(+), 1 deletion(-)

diff --cc CHANGES.txt
index ea45e19dfd,c6dbd85e4f..8d2d52ecc9
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@@ -1,23 -1,6 +1,24 @@@
 -4.1.13
 +5.0.10
 + * Replica filtering protection resurrects rows deleted by a range tombstone 
that spans pages (CASSANDRA-21781)
 + * Write an sstable with the compression its components were chosen for when 
ALTER TABLE changes it (CASSANDRA-21780)
 + * Clarify the unset, true and false behaviours of 
cassandra.reset_bootstrap_progress in the abort message and docs 
(CASSANDRA-21595)
 + * Isolate SAI index rebuilds from ZCS (CASSANDRA-21520)
 + * Bound and chunk the BTI partition index preload (CASSANDRA-21632)
 + * Restore volatile on CommitLogSegment.cdcState dropped in merge 
(CASSANDRA-21665)
 + * Fix compressed scan read-ahead buffer Block leak and chunk-cache pollution 
by scans (CASSANDRA-21671)
 + * A read on a replica that does not know the index yet fails with a 
NullPointerException (CASSANDRA-21658)
 + * Release hints write buffer when the write executor shuts down 
(CASSANDRA-21657)
 + * Fix concurrent SAI vector inserts failing once jvector's per-graph pool 
limit is exceeded (CASSANDRA-21644)
 + * Let the commit log allocator observe the shutdown state (CASSANDRA-21616)
 + * Unwrap LongType properly when calculating min/max terms in V1SSTableIndex 
(CASSANDRA-21635)
 + * Force repair should ignore min_repair_interval (CASSANDRA-21552)
 + * Render SubnetGroups as JSON in system_views.settings (CASSANDRA-21579)
 + * Avoid rebuilding per-SSTable SAI components unless missing or corrupted 
(CASSANDRA-21515)
 + * Propagate trickle_fsync settings to compressed SSTable writers 
(CASSANDRA-21487)
 + * Allow DatabaseDescriptor.setCompressedReadAheadBufferSizeInKb(0) to 
disable read-ahead buffer (CASSANDRA-21522)
 + * Return CorruptSSTableException if chunk metadata and file size are out of 
sync (CASSANDRA-21519)
  Merged from 4.0:
+  * Validate Digest.crc32 when zero-copy streaming SSTables (CASSANDRA-21694)
   * Avoid DNS lookup for hostname column in ClientsTable and GossipInfoTable 
(CASSANDRA-21539)
   * Separate memtable memory usage tracking from limit checking, and fixes a a 
flush writeBarrier deadlock (CASSANDRA-21019)
   * Fixes for fully-expired SSTable index notification in compaction 
(CASSANDRA-21577)
diff --cc src/java/org/apache/cassandra/config/Config.java
index 87cc553176,a59b0ad8e1..dca5f03a4e
--- a/src/java/org/apache/cassandra/config/Config.java
+++ b/src/java/org/apache/cassandra/config/Config.java
@@@ -698,7 -661,11 +698,12 @@@ public class Confi
      public volatile int max_concurrent_automatic_sstable_upgrades = 1;
      public boolean stream_entire_sstables = true;
  
+     /**
+      * When receiving a zero-copy stream, validate Digest.crc32 before the 
sstable is made visible.
+      */
+     public volatile boolean entire_sstable_stream_digest_validation_enabled = 
false;
+ 
 +    public volatile boolean skip_stream_disk_space_check = false;
  
      public volatile AuditLogOptions audit_logging_options = new 
AuditLogOptions();
      public volatile FullQueryLoggerOptions full_query_logging_options = new 
FullQueryLoggerOptions();
diff --cc src/java/org/apache/cassandra/config/DatabaseDescriptor.java
index c4fcc4ebef,6c20225b8d..d8e428b643
--- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
+++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
@@@ -4019,26 -3537,16 +4019,36 @@@ public class DatabaseDescripto
          return conf.stream_entire_sstables;
      }
  
 +    @VisibleForTesting
 +    public static boolean setStreamEntireSSTables(boolean value)
 +    {
 +        return conf.stream_entire_sstables = value;
 +    }
 +
+     public static boolean getEntireSSTableStreamDigestValidationEnabled()
+     {
+         return conf.entire_sstable_stream_digest_validation_enabled;
+     }
+ 
+     public static void setEntireSSTableStreamDigestValidationEnabled(boolean 
value)
+     {
+         conf.entire_sstable_stream_digest_validation_enabled = value;
+     }
+ 
 +    public static DurationSpec.LongMillisecondsBound 
getStreamTransferTaskTimeout()
 +    {
 +        return conf.stream_transfer_task_timeout;
 +    }
 +
 +    public static boolean getSkipStreamDiskSpaceCheck()
 +    {
 +        return conf.skip_stream_disk_space_check;
 +    }
 +
 +    public static void setSkipStreamDiskSpaceCheck(boolean value)
 +    {
 +        conf.skip_stream_disk_space_check = value;
 +    }
  
      public static String getLocalDataCenter()
      {
diff --cc 
src/java/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamReader.java
index 97c3b2d4f9,c876926158..81a82c86cf
--- 
a/src/java/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamReader.java
+++ 
b/src/java/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamReader.java
@@@ -29,16 -31,15 +31,18 @@@ import org.apache.cassandra.config.Data
  import org.apache.cassandra.db.ColumnFamilyStore;
  import org.apache.cassandra.db.Directories;
  import org.apache.cassandra.db.lifecycle.LifecycleNewTracker;
 +import org.apache.cassandra.io.compress.BufferType;
  import org.apache.cassandra.io.sstable.Component;
  import org.apache.cassandra.io.sstable.Descriptor;
 +import org.apache.cassandra.io.sstable.IOOptions;
  import org.apache.cassandra.io.sstable.SSTableMultiWriter;
 -import org.apache.cassandra.io.sstable.format.SSTableFormat;
 -import org.apache.cassandra.io.sstable.format.big.BigTableZeroCopyWriter;
 +import org.apache.cassandra.io.sstable.SSTableZeroCopyWriter;
++import org.apache.cassandra.io.sstable.format.SSTableFormat.Components;
  import org.apache.cassandra.io.sstable.metadata.StatsMetadata;
  import org.apache.cassandra.io.util.DataInputPlus;
+ import org.apache.cassandra.io.util.DataIntegrityMetadata;
  import org.apache.cassandra.io.util.File;
 +import org.apache.cassandra.io.util.SequentialWriterOption;
  import org.apache.cassandra.schema.TableId;
  import org.apache.cassandra.streaming.ProgressInfo;
  import org.apache.cassandra.streaming.StreamReceiver;
@@@ -153,6 -157,37 +160,38 @@@ public class CassandraEntireSSTableStre
          }
      }
  
+     /**
+      * Validates the received {@code Data.db} against {@code Digest.crc32}.
+      * @throws IOException if a digest was streamed and the received data 
file does not match it
+      */
+     private void validateDataDigest(Descriptor descriptor, ComponentManifest 
manifest) throws IOException
+     {
+         if 
(!DatabaseDescriptor.getEntireSSTableStreamDigestValidationEnabled())
+             return;
+ 
+         List<Component> components = manifest.components();
 -        if (!components.contains(Component.DATA) || 
!components.contains(Component.DIGEST))
++        if (!components.contains(Components.DATA) || 
!components.contains(Components.DIGEST))
+         {
+             NoSpamLogger.log(logger, NoSpamLogger.Level.WARN, 1, 
TimeUnit.MINUTES,
+                              "Accepting zero-copy sstable stream from {} 
without validating CRC due to " +
+                              "missing {} or {} from streamed components: {}",
 -                             session.peer, Component.DATA, Component.DIGEST, 
components);
++                             session.peer, Components.DATA, 
Components.DIGEST, components);
+             return;
+         }
+ 
 -        try (DataIntegrityMetadata.FileDigestValidator validator = new 
DataIntegrityMetadata.FileDigestValidator(descriptor))
++        try
+         {
 -            validator.validate();
++            new 
DataIntegrityMetadata.FileDigestValidator(descriptor.fileFor(Components.DATA),
++                                                          
descriptor.fileFor(Components.DIGEST)).validate();
+         }
+         catch (IOException e)
+         {
+             session.countEntireSSTableDigestMismatch();
+             throw new IOException(format("[Stream #%s] Digest mismatch for %s 
received from %s, aborting stream",
+                                          session.planId(), descriptor, 
session.peer), e);
+         }
+     }
+ 
      private File getDataDir(ColumnFamilyStore cfs, long totalSize) throws 
IOException
      {
          Directories.DataDirectory localDir = 
cfs.getDirectories().getWriteableLocation(totalSize);
diff --cc src/java/org/apache/cassandra/io/util/DataIntegrityMetadata.java
index aef3614da6,65d4e58f0f..7805a87f34
--- a/src/java/org/apache/cassandra/io/util/DataIntegrityMetadata.java
+++ b/src/java/org/apache/cassandra/io/util/DataIntegrityMetadata.java
@@@ -103,17 -142,21 +103,25 @@@ public class DataIntegrityMetadat
          // Validate the entire file
          public void validate() throws IOException
          {
 -            CheckedInputStream checkedInputStream = new 
CheckedInputStream(dataReader, checksum);
 -            byte[] chunk = new byte[64 * 1024];
 -
 -            while( checkedInputStream.read(chunk) > 0 ) { }
 -            long calculatedDigestValue = 
checkedInputStream.getChecksum().getValue();
 -            if (storedDigestValue != calculatedDigestValue)
 +            try (RandomAccessReader digestReader = 
RandomAccessReader.open(digestFile);
 +                 RandomAccessReader dataReader = 
RandomAccessReader.open(dataFile);
 +                 CheckedInputStream checkedInputStream = new 
CheckedInputStream(dataReader, checksum);)
              {
-                 long storedDigestValue = 
Long.parseLong(digestReader.readLine());
 -                throw new IOException("Corrupted SSTable : " + 
descriptor.filenameFor(Component.DATA));
++                long storedDigestValue;
++                try
++                {
++                    storedDigestValue = 
Long.parseLong(digestReader.readLine());
++                }
++                catch (NumberFormatException e)
++                {
++                    throw new IOException(String.format("Corrupted file: 
invalid digest value in %s", digestFile), e);
++                }
 +                byte[] chunk = new byte[64 * 1024];
 +                while (checkedInputStream.read(chunk) > 0) ;
 +                long calculatedDigestValue = 
checkedInputStream.getChecksum().getValue();
 +                if (storedDigestValue != calculatedDigestValue)
 +                    throw new IOException(String.format("Corrupted file: 
integrity check (digest) failed for %s: %d != %d", dataFile, storedDigestValue, 
calculatedDigestValue));
              }
          }
 -
 -        public void close()
 -        {
 -            Throwables.perform(digestReader::close,
 -                               dataReader::close);
 -        }
      }
  }
diff --cc 
test/distributed/org/apache/cassandra/distributed/test/hostreplacement/HostReplacementTest.java
index 8219d43ad1,fa95a8aebf..c36e83ce16
--- 
a/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/HostReplacementTest.java
+++ 
b/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/HostReplacementTest.java
@@@ -35,8 -39,9 +39,10 @@@ import org.apache.cassandra.distributed
  import org.apache.cassandra.distributed.api.SimpleQueryResult;
  import org.apache.cassandra.distributed.api.TokenSupplier;
  import org.apache.cassandra.distributed.shared.AssertUtils;
 +import org.apache.cassandra.distributed.shared.ClusterUtils;
  import org.apache.cassandra.distributed.test.TestBaseImpl;
 -import org.apache.cassandra.io.sstable.Component;
++import org.apache.cassandra.io.sstable.format.SSTableFormat.Components;
+ import org.apache.cassandra.io.sstable.format.SSTableReader;
  import org.assertj.core.api.Assertions;
  
  import static 
org.apache.cassandra.config.CassandraRelevantProperties.BOOTSTRAP_SKIP_SCHEMA_CHECK;
@@@ -206,6 -211,83 +212,83 @@@ public class HostReplacementTest extend
          }
      }
  
+     private static final int HEALTHY_PK = 10;
+     private static final int CORRUPT_PK = 11;
+ 
+     /**
+      * With digest validation enabled, a single corrupted sstable must fail 
the entire stream session, so neither
+      * the corrupted sstable nor its healthy sibling should land on the 
replacing node.
+      */
+     @Test
+     public void 
replaceHostAbortsStreamingWhenDigestValidationCatchesCorruption() throws 
IOException, TimeoutException
+     {
+         TokenSupplier even = TokenSupplier.evenlyDistributedTokens(2);
+         try (Cluster cluster = Cluster.build(2)
+                                       .withConfig(c -> c.with(Feature.GOSSIP, 
Feature.NETWORK)
+                                                         
.set("entire_sstable_stream_digest_validation_enabled", true))
+                                       .withTokenSupplier(node -> 
even.token(node == 3 ? 2 : node))
+                                       .start())
+         {
+             IInvokableInstance node1 = cluster.get(1);
+             IInvokableInstance nodeToRemove = cluster.get(2);
+ 
+             setupCluster(cluster);
+             writeHealthyAndCorruptSSTables(cluster, node1);
+ 
+             stopUnchecked(nodeToRemove);
+ 
+             IInvokableInstance replacingNode = replaceHostAndStart(cluster, 
nodeToRemove);
+ 
+             replacingNode.logs().watchFor("Digest mismatch");
+ 
+             // the whole stream session is aborted on a digest mismatch, so 
neither sstable should have landed
+             AssertUtils.assertRows(replacingNode.executeInternal("SELECT * 
FROM " + KEYSPACE + ".tbl WHERE pk = " + HEALTHY_PK), EMPTY_ROWS);
+             AssertUtils.assertRows(replacingNode.executeInternal("SELECT * 
FROM " + KEYSPACE + ".tbl WHERE pk = " + CORRUPT_PK), EMPTY_ROWS);
+         }
+     }
+ 
+     /**
+      * Flushes two separate sstables on {@code node1} for {@code 
KEYSPACE.tbl}: one containing {@link #HEALTHY_PK}
+      * left untouched, and one containing {@link #CORRUPT_PK} that gets its 
Data.db corrupted.
+      */
+     static void writeHealthyAndCorruptSSTables(Cluster cluster, 
IInvokableInstance node1) throws IOException
+     {
+         node1.nodetoolResult("disableautocompaction").asserts().success();
+ 
+         cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl (pk) 
VALUES (?)",
+                                        ConsistencyLevel.ALL,
+                                        HEALTHY_PK);
+         node1.flush(KEYSPACE);
+ 
+         cluster.coordinator(1).execute("INSERT INTO " + KEYSPACE + ".tbl (pk) 
VALUES (?)",
+                                        ConsistencyLevel.ALL,
+                                        CORRUPT_PK);
+         node1.flush(KEYSPACE);
+ 
+         corruptSSTable(node1, "tbl");
+     }
+ 
+     /** Flips a byte in the middle of the Data.db file of the most recently 
flushed sstable for the given table. */
+     static void corruptSSTable(IInvokableInstance instance, String table) 
throws IOException
+     {
+         String dataFilePath = instance.callOnInstance(() -> {
+             ColumnFamilyStore cfs = 
Keyspace.open(KEYSPACE).getColumnFamilyStore(table);
+             SSTableReader sstable = cfs.getLiveSSTables().stream()
+                                        
.max(SSTableReader.maxTimestampAscending)
 -                                       .get();
 -            return sstable.descriptor.fileFor(Component.DATA).absolutePath();
++                                       .orElseThrow();
++            return sstable.descriptor.fileFor(Components.DATA).absolutePath();
+         });
+ 
+         try (RandomAccessFile raf = new RandomAccessFile(dataFilePath, "rw"))
+         {
+             long corruptAt = raf.length() / 2;
+             raf.seek(corruptAt);
+             int b = raf.read();
+             raf.seek(corruptAt);
+             raf.write(b ^ 0xFF);
+         }
+     }
+ 
      static void setupCluster(Cluster cluster)
      {
          fixDistributedSchemas(cluster);
diff --cc 
test/unit/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamWriterTest.java
index 89a3e9ed36,5184b5ae3c..dcafae0058
--- 
a/test/unit/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamWriterTest.java
+++ 
b/test/unit/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamWriterTest.java
@@@ -22,8 -23,15 +23,10 @@@ import java.nio.ByteBuffer
  import java.nio.channels.WritableByteChannel;
  import java.util.Collection;
  import java.util.Collections;
+ import java.util.LinkedHashMap;
  import java.util.Queue;
  
 -import com.google.common.io.ByteStreams;
 -
 -import org.apache.cassandra.Util;
 -import org.apache.cassandra.io.sstable.Component;
 -import org.apache.cassandra.io.sstable.Descriptor;
+ import org.junit.AfterClass;
  import org.junit.BeforeClass;
  import org.junit.Test;
  
@@@ -35,13 -45,11 +38,16 @@@ import io.netty.channel.ChannelPromise
  import io.netty.channel.DefaultFileRegion;
  import io.netty.channel.embedded.EmbeddedChannel;
  import org.apache.cassandra.SchemaLoader;
 +import org.apache.cassandra.Util;
++import org.apache.cassandra.config.DatabaseDescriptor;
  import org.apache.cassandra.db.ColumnFamilyStore;
  import org.apache.cassandra.db.Keyspace;
  import org.apache.cassandra.db.RowUpdateBuilder;
  import org.apache.cassandra.db.compaction.CompactionManager;
++import org.apache.cassandra.io.sstable.Component;
 +import org.apache.cassandra.io.sstable.Descriptor;
  import org.apache.cassandra.io.sstable.SSTableMultiWriter;
++import org.apache.cassandra.io.sstable.format.SSTableFormat.Components;
  import org.apache.cassandra.io.sstable.format.SSTableReader;
  import org.apache.cassandra.io.util.DataInputBuffer;
  import org.apache.cassandra.locator.InetAddressAndPort;
@@@ -169,6 -189,125 +186,124 @@@ public class CassandraEntireSSTableStre
          }
      }
  
+     @Test
+     public void testCorruptDataComponentFailsDigestValidation() throws 
Throwable
+     {
+         ByteBuf serializedFile = Unpooled.buffer(8192);
+         ComponentManifest manifest = writeEntireSSTable(serializedFile);
+         corruptDataComponent(serializedFile, manifest);
+ 
+         
DatabaseDescriptor.setEntireSSTableStreamDigestValidationEnabled(true);
+         assertThatThrownBy(() -> readEntireSSTable(serializedFile, manifest))
+         .isInstanceOf(IOException.class)
+         .hasMessageContaining("Digest mismatch")
+         .hasCauseInstanceOf(IOException.class);
+     }
+ 
+     @Test
+     public void testCorruptDataComponentAcceptedWhenValidationDisabled() 
throws Throwable
+     {
+         ByteBuf serializedFile = Unpooled.buffer(8192);
+         ComponentManifest manifest = writeEntireSSTable(serializedFile);
+         corruptDataComponent(serializedFile, manifest);
+ 
+         
DatabaseDescriptor.setEntireSSTableStreamDigestValidationEnabled(false);
+ 
+         // shouldn't throw an error
+         SSTableMultiWriter writer = readEntireSSTable(serializedFile, 
manifest);
+         assertNotNull(writer);
+         writer.abort(null);
+     }
+ 
+     @Test
+     public void testMissingDigestIsAcceptedWithoutValidation() throws 
Throwable
+     {
+         
DatabaseDescriptor.setEntireSSTableStreamDigestValidationEnabled(true);
+ 
+         ComponentManifest full;
 -        try (ComponentContext context = ComponentContext.create(descriptor))
++        try (ComponentContext context = ComponentContext.create(sstable))
+         {
+             full = context.manifest();
+         }
+ 
+         // remove digest component from sstable
+         LinkedHashMap<Component, Long> withoutDigest = new LinkedHashMap<>();
+         for (Component component : full.components())
+         {
 -            if (!component.equals(Component.DIGEST))
++            if (!component.equals(Components.DIGEST))
+                 withoutDigest.put(component, full.sizeOf(component));
+         }
+         ComponentManifest manifest = new ComponentManifest(withoutDigest);
+ 
+         // stream from live files
+         ByteBuf serializedFile = Unpooled.buffer(8192);
+         for (Component component : manifest.components())
+         {
+             try (InputStream in = 
descriptor.fileFor(component).newInputStream())
+             {
 -                serializedFile.writeBytes(ByteStreams.toByteArray(in));
++                serializedFile.writeBytes(in.readAllBytes());
+             }
+         }
+ 
+         // shouldn't throw an error
+         SSTableMultiWriter writer = readEntireSSTable(serializedFile, 
manifest);
+         assertNotNull(writer);
+         writer.abort(null);
+     }
+ 
+     private ComponentManifest writeEntireSSTable(ByteBuf serializedFile) 
throws Exception
+     {
+         StreamSession session = setupStreamingSessionForTest();
+         EmbeddedChannel channel = createMockNettyChannel(serializedFile);
+         try (AsyncStreamingOutputPlus out = new 
AsyncStreamingOutputPlus(channel);
 -             ComponentContext context = ComponentContext.create(descriptor))
++             ComponentContext context = ComponentContext.create(sstable))
+         {
+             new CassandraEntireSSTableStreamWriter(sstable, session, 
context).write(out);
+             return context.manifest();
+         }
+     }
+ 
+     private SSTableMultiWriter readEntireSSTable(ByteBuf serializedFile, 
ComponentManifest manifest) throws Throwable
+     {
+         StreamSession session = setupStreamingSessionForTest();
+         InetAddressAndPort peer = FBUtilities.getBroadcastAddressAndPort();
+         session.prepareReceiving(new StreamSummary(sstable.metadata().id, 1, 
5104));
+ 
+         CassandraStreamHeader header =
+         CassandraStreamHeader.builder()
 -                             .withSSTableFormat(sstable.descriptor.formatType)
+                              .withSSTableVersion(sstable.descriptor.version)
+                              .withSSTableLevel(0)
+                              .withEstimatedKeys(sstable.estimatedKeys())
+                              .withSections(Collections.emptyList())
+                              
.withSerializationHeader(sstable.header.toComponent())
+                              .withComponentManifest(manifest)
+                              .isEntireSSTable(true)
 -                             .withFirstKey(sstable.first)
++                             .withFirstKey(sstable.getFirst())
+                              .withTableId(sstable.metadata().id)
+                              .build();
+ 
+         CassandraEntireSSTableStreamReader reader =
+         new CassandraEntireSSTableStreamReader(new 
StreamMessageHeader(sstable.metadata().id, peer, session.planId(), false, 0, 0, 
0, null),
+                                                header, session);
+ 
+         return reader.read(new DataInputBuffer(serializedFile.nioBuffer(), 
false));
+     }
+ 
+     /** Flips every bit of one byte in the middle of the Data.db region of an 
already-serialized stream. */
+     private static void corruptDataComponent(ByteBuf serializedFile, 
ComponentManifest manifest)
+     {
+         long dataOffset = 0;
+         for (Component component : manifest.components())
+         {
 -            if (component.equals(Component.DATA))
++            if (component.equals(Components.DATA))
+                 break;
+             dataOffset += manifest.sizeOf(component);
+         }
+ 
 -        int corruptAt = (int) (dataOffset + manifest.sizeOf(Component.DATA) / 
2);
++        int corruptAt = (int) (dataOffset + manifest.sizeOf(Components.DATA) 
/ 2);
+         serializedFile.setByte(corruptAt, serializedFile.getByte(corruptAt) ^ 
0xff);
+     }
+ 
      private EmbeddedChannel createMockNettyChannel(ByteBuf serializedFile) 
throws Exception
      {
          WritableByteChannel wbc = new WritableByteChannel()
diff --cc test/unit/org/apache/cassandra/io/util/DataIntegrityMetadataTest.java
index 0000000000,0000000000..9a5e2f3e23
new file mode 100644
--- /dev/null
+++ b/test/unit/org/apache/cassandra/io/util/DataIntegrityMetadataTest.java
@@@ -1,0 -1,0 +1,72 @@@
++/*
++ * Licensed to the Apache Software Foundation (ASF) under one
++ * or more contributor license agreements.  See the NOTICE file
++ * distributed with this work for additional information
++ * regarding copyright ownership.  The ASF licenses this file
++ * to you under the Apache License, Version 2.0 (the
++ * "License"); you may not use this file except in compliance
++ * with the License.  You may obtain a copy of the License at
++ *
++ *     http://www.apache.org/licenses/LICENSE-2.0
++ *
++ * Unless required by applicable law or agreed to in writing, software
++ * distributed under the License is distributed on an "AS IS" BASIS,
++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++ * See the License for the specific language governing permissions and
++ * limitations under the License.
++ */
++package org.apache.cassandra.io.util;
++
++import java.io.IOException;
++import java.nio.charset.StandardCharsets;
++import java.nio.file.Files;
++
++import org.junit.BeforeClass;
++import org.junit.Rule;
++import org.junit.Test;
++import org.junit.rules.TemporaryFolder;
++
++import org.apache.cassandra.config.DatabaseDescriptor;
++
++import static org.assertj.core.api.Assertions.assertThatThrownBy;
++import static org.junit.Assert.assertEquals;
++
++public class DataIntegrityMetadataTest
++{
++    @BeforeClass
++    public static void setupDD()
++    {
++        DatabaseDescriptor.daemonInitialization();
++    }
++
++    @Rule
++    public TemporaryFolder tempDir = new TemporaryFolder();
++
++    @Test
++    public void testNonNumericDigestValueFailsWithClearError() throws 
IOException
++    {
++        assertInvalidDigestValueFailsWithClearError("not-a-number");
++    }
++
++    @Test
++    public void testEmptyDigestFileFailsWithClearError() throws IOException
++    {
++        assertInvalidDigestValueFailsWithClearError("");
++    }
++
++    private void assertInvalidDigestValueFailsWithClearError(String 
digestContent) throws IOException
++    {
++        File dataFile = new File(tempDir.newFile("data.db"));
++        Files.write(dataFile.toPath(), "some 
bytes".getBytes(StandardCharsets.UTF_8));
++
++        File digestFile = new File(tempDir.newFile("data.crc32"));
++        Files.write(digestFile.toPath(), 
digestContent.getBytes(StandardCharsets.UTF_8));
++        assertEquals("digest file fixture doesn't contain what this test 
expects",
++                     digestContent.getBytes(StandardCharsets.UTF_8).length, 
digestFile.length());
++
++        assertThatThrownBy(() -> new 
DataIntegrityMetadata.FileDigestValidator(dataFile, digestFile).validate())
++        .isInstanceOf(IOException.class)
++        .hasMessageContaining("Corrupted file: invalid digest value in " + 
digestFile)
++        .hasCauseInstanceOf(NumberFormatException.class);
++    }
++}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to