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

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


The following commit(s) were added to refs/heads/cassandra-4.0 by this push:
     new 80578c2df5 Validate Digest.crc32 when zero-copy streaming SSTables
80578c2df5 is described below

commit 80578c2df5a394b275432da83be53d6ddcc7fc82
Author: nivy <[email protected]>
AuthorDate: Fri Sep 18 21:54:09 2026 -0400

    Validate Digest.crc32 when zero-copy streaming SSTables
    
    Patch by Nivy Kani; reviewed by Caleb Rackliffe and marcuse for 
CASSANDRA-21694
---
 CHANGES.txt                                        |   1 +
 conf/cassandra.yaml                                |   8 ++
 src/java/org/apache/cassandra/config/Config.java   |   6 +
 .../cassandra/config/DatabaseDescriptor.java       |  11 ++
 .../CassandraEntireSSTableStreamReader.java        |  38 ++++++
 .../apache/cassandra/metrics/StreamingMetrics.java |   9 ++
 .../apache/cassandra/streaming/StreamSession.java  |   6 +
 .../test/hostreplacement/HostReplacementTest.java  |  83 +++++++++++++
 .../CassandraEntireSSTableStreamWriterTest.java    | 137 +++++++++++++++++++++
 9 files changed, 299 insertions(+)

diff --git a/CHANGES.txt b/CHANGES.txt
index 95c3861d2f..7fcbc24dce 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 4.0.22
+ * 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 --git a/conf/cassandra.yaml b/conf/cassandra.yaml
index 14f11587a8..e7bb1c65ad 100644
--- a/conf/cassandra.yaml
+++ b/conf/cassandra.yaml
@@ -942,6 +942,14 @@ sstable_preemptive_open_interval_in_mb: 50
 # disabled if internode encryption is enabled.
 # stream_entire_sstables: true
 
+# When an entire SSTable is received via zero-copy streaming, validate the 
Digest.crc32 that was streamed with it
+# against the received Data.db before the SSTable is added to the table. This 
detects corruption introduced in
+# transit as well as pre-existing corruption on the sending node's disk, 
neither of which is otherwise noticed by
+# this path, since it writes component bytes to disk without parsing them.
+# If there's no Digest.crc32, this check passes.
+# When unset, the default is not validating Digest.crc32.
+# entire_sstable_stream_digest_validation_enabled: false
+
 # Throttles all outbound streaming file transfers on this node to the
 # given total throughput in Mbps. This is necessary because Cassandra does
 # mostly sequential IO when streaming data during bootstrap or repair, which
diff --git a/src/java/org/apache/cassandra/config/Config.java 
b/src/java/org/apache/cassandra/config/Config.java
index 24df356a4b..2611379a8f 100644
--- a/src/java/org/apache/cassandra/config/Config.java
+++ b/src/java/org/apache/cassandra/config/Config.java
@@ -490,6 +490,12 @@ public class Config
     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 AuditLogOptions audit_logging_options = new 
AuditLogOptions();
     public volatile FullQueryLoggerOptions full_query_logging_options = new 
FullQueryLoggerOptions();
 
diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java 
b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
index 00a4aa912f..8aa01c53d4 100644
--- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
+++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
@@ -2860,6 +2860,17 @@ public class DatabaseDescriptor
         return conf.stream_entire_sstables;
     }
 
+    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 String getLocalDataCenter()
     {
         return localDC;
diff --git 
a/src/java/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamReader.java
 
b/src/java/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamReader.java
index 0bfe993112..dadc026f3a 100644
--- 
a/src/java/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamReader.java
+++ 
b/src/java/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamReader.java
@@ -21,6 +21,8 @@ package org.apache.cassandra.db.streaming;
 import java.io.File;
 import java.io.IOException;
 import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
 import java.util.function.UnaryOperator;
 
 import com.google.common.base.Throwables;
@@ -28,6 +30,7 @@ import org.apache.cassandra.db.lifecycle.LifecycleNewTracker;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.db.ColumnFamilyStore;
 import org.apache.cassandra.db.Directories;
 import org.apache.cassandra.io.sstable.Component;
@@ -37,11 +40,13 @@ import org.apache.cassandra.io.sstable.format.SSTableFormat;
 import org.apache.cassandra.io.sstable.format.big.BigTableZeroCopyWriter;
 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.schema.TableId;
 import org.apache.cassandra.streaming.ProgressInfo;
 import org.apache.cassandra.streaming.StreamReceiver;
 import org.apache.cassandra.streaming.StreamSession;
 import org.apache.cassandra.streaming.messages.StreamMessageHeader;
+import org.apache.cassandra.utils.NoSpamLogger;
 
 import static java.lang.String.format;
 import static org.apache.cassandra.utils.FBUtilities.prettyPrintMemory;
@@ -135,6 +140,8 @@ public class CassandraEntireSSTableStreamReader implements 
IStreamReader
                              prettyPrintMemory(totalSize));
             }
 
+            validateDataDigest(writer.descriptor, manifest);
+
             UnaryOperator<StatsMetadata> transform = stats -> 
stats.mutateLevel(header.sstableLevel)
                                                                    
.mutateRepairedMetadata(messageHeader.repairedAt, messageHeader.pendingRepair, 
false);
             String description = String.format("level %s and repairedAt time 
%s and pendingRepair %s",
@@ -152,6 +159,37 @@ public class CassandraEntireSSTableStreamReader implements 
IStreamReader
         }
     }
 
+    /**
+     * 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))
+        {
+            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);
+            return;
+        }
+
+        try (DataIntegrityMetadata.FileDigestValidator validator = new 
DataIntegrityMetadata.FileDigestValidator(descriptor))
+        {
+            validator.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 --git a/src/java/org/apache/cassandra/metrics/StreamingMetrics.java 
b/src/java/org/apache/cassandra/metrics/StreamingMetrics.java
index 54df23357e..0fe1f76f88 100644
--- a/src/java/org/apache/cassandra/metrics/StreamingMetrics.java
+++ b/src/java/org/apache/cassandra/metrics/StreamingMetrics.java
@@ -47,6 +47,7 @@ public class StreamingMetrics
     public final Counter outgoingBytes;
     /* Measures the time taken for processing the incoming stream message 
after being deserialized, including the time to flush to disk. */
     public final Timer incomingProcessTime;
+    private final Counter entireSSTableDigestMismatches;
 
     public static StreamingMetrics get(InetAddressAndPort ip)
     {
@@ -79,5 +80,13 @@ public class StreamingMetrics
         incomingBytes = 
Metrics.counter(factory.createMetricName("IncomingBytes"));
         outgoingBytes= 
Metrics.counter(factory.createMetricName("OutgoingBytes"));
         incomingProcessTime = 
Metrics.timer(factory.createMetricName("IncomingProcessTime"));
+
+        entireSSTableDigestMismatches = 
Metrics.counter(factory.createMetricName("EntireSSTableDigestMismatches"));
+    }
+
+    /** An entire-sstable stream was rejected because the received data file 
did not match the streamed digest. */
+    public void countEntireSSTableDigestMismatch()
+    {
+        entireSSTableDigestMismatches.inc();
     }
 }
diff --git a/src/java/org/apache/cassandra/streaming/StreamSession.java 
b/src/java/org/apache/cassandra/streaming/StreamSession.java
index 786311b1d8..0d2668c927 100644
--- a/src/java/org/apache/cassandra/streaming/StreamSession.java
+++ b/src/java/org/apache/cassandra/streaming/StreamSession.java
@@ -754,6 +754,12 @@ public class StreamSession implements 
IEndpointStateChangeSubscriber
         });
     }
 
+    public void countEntireSSTableDigestMismatch()
+    {
+        metrics.countEntireSSTableDigestMismatch();
+    }
+
+
     /**
      * Finish preparing the session. This method is blocking (memtables are 
flushed in {@link #addTransferRanges}),
      * so the logic should not execute on the main IO thread (read: netty 
event loop).
diff --git 
a/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/HostReplacementTest.java
 
b/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/HostReplacementTest.java
index 3de0bf51d5..941174f681 100644
--- 
a/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/HostReplacementTest.java
+++ 
b/test/distributed/org/apache/cassandra/distributed/test/hostreplacement/HostReplacementTest.java
@@ -19,13 +19,17 @@
 package org.apache.cassandra.distributed.test.hostreplacement;
 
 import java.io.IOException;
+import java.io.RandomAccessFile;
 import java.util.Arrays;
 import java.util.List;
+import java.util.concurrent.TimeoutException;
 
 import org.junit.Test;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import org.apache.cassandra.db.ColumnFamilyStore;
+import org.apache.cassandra.db.Keyspace;
 import org.apache.cassandra.distributed.Cluster;
 import org.apache.cassandra.distributed.Constants;
 import org.apache.cassandra.distributed.api.ConsistencyLevel;
@@ -36,6 +40,8 @@ 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.test.TestBaseImpl;
+import org.apache.cassandra.io.sstable.Component;
+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;
@@ -205,6 +211,83 @@ public class HostReplacementTest extends TestBaseImpl
         }
     }
 
+    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.filenameFor(Component.DATA);
+        });
+
+        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 --git 
a/test/unit/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamWriterTest.java
 
b/test/unit/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamWriterTest.java
index 58d26c11ad..43099cfed3 100644
--- 
a/test/unit/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamWriterTest.java
+++ 
b/test/unit/org/apache/cassandra/db/streaming/CassandraEntireSSTableStreamWriterTest.java
@@ -17,18 +17,26 @@
  */
 package org.apache.cassandra.db.streaming;
 
+import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStream;
 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 java.util.UUID;
 
+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;
 
+import org.apache.cassandra.config.DatabaseDescriptor;
+
+import com.google.common.io.ByteStreams;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelHandlerContext;
@@ -62,7 +70,9 @@ import 
org.apache.cassandra.streaming.messages.StreamMessageHeader;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.FBUtilities;
 
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 public class CassandraEntireSSTableStreamWriterTest
@@ -75,11 +85,14 @@ public class CassandraEntireSSTableStreamWriterTest
     private static SSTableReader sstable;
     private static Descriptor descriptor;
     private static ColumnFamilyStore store;
+    private static boolean originalDigestValidationEnabled;
 
     @BeforeClass
     public static void defineSchemaAndPrepareSSTable()
     {
         SchemaLoader.prepareServer();
+        originalDigestValidationEnabled = 
DatabaseDescriptor.getEntireSSTableStreamDigestValidationEnabled();
+
         SchemaLoader.createKeyspace(KEYSPACE,
                                     KeyspaceParams.simple(1),
                                     SchemaLoader.standardCFMD(KEYSPACE, 
CF_STANDARD),
@@ -109,6 +122,12 @@ public class CassandraEntireSSTableStreamWriterTest
         descriptor = sstable.descriptor;
     }
 
+    @AfterClass
+    public static void restoreDigestValidationFlag()
+    {
+        
DatabaseDescriptor.setEntireSSTableStreamDigestValidationEnabled(originalDigestValidationEnabled);
+    }
+
     @Test
     public void testBlockWriterOverWire() throws IOException
     {
@@ -169,6 +188,124 @@ public class CassandraEntireSSTableStreamWriterTest
         }
     }
 
+    @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))
+        .hasCauseInstanceOf(IOException.class)
+        .hasStackTraceContaining("Digest mismatch");
+    }
+
+    @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))
+        {
+            full = context.manifest();
+        }
+
+        // remove digest component from sstable
+        LinkedHashMap<Component, Long> withoutDigest = new LinkedHashMap<>();
+        for (Component component : full.components())
+        {
+            if (!component.equals(Component.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 = new 
FileInputStream(descriptor.filenameFor(component)))
+            {
+                serializedFile.writeBytes(ByteStreams.toByteArray(in));
+            }
+        }
+
+        // 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))
+        {
+            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)
+                             .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))
+                break;
+            dataOffset += manifest.sizeOf(component);
+        }
+
+        int corruptAt = (int) (dataOffset + manifest.sizeOf(Component.DATA) / 
2);
+        serializedFile.setByte(corruptAt, serializedFile.getByte(corruptAt) ^ 
0xff);
+    }
+
     private EmbeddedChannel createMockNettyChannel(ByteBuf serializedFile) 
throws Exception
     {
         WritableByteChannel wbc = new WritableByteChannel()


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

Reply via email to