Skip to content

#757 Add support for transparent GPG/PGP decryption of files on read - #868

Merged
yruslan merged 6 commits into
masterfrom
feature/757-gpg-decryption
Aug 5, 2026
Merged

#757 Add support for transparent GPG/PGP decryption of files on read#868
yruslan merged 6 commits into
masterfrom
feature/757-gpg-decryption

Conversation

@yruslan

@yruslan yruslan commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Closes #757

Summary by CodeRabbit

  • New Features

    • Added experimental support for transparently reading GPG/PGP-encrypted COBOL data files.
    • Added configuration options for private keys and optional passphrases.
    • Encrypted files now work with fixed-length, variable-length, indexed, and compressed data processing.
  • Bug Fixes

    • Corrected wording in a GROUP-field extraction error message.
  • Tests

    • Added coverage and sample data for encrypted-file processing.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Cobrix now accepts optional ASCII-armored GPG credentials through reader options. It decrypts GPG input streams before compression handling and propagates credentials through Spark processing, indexing, and scanning. The change adds Bouncy Castle support, documentation, fixtures, and tests.

Changes

Transparent PGP decryption

Layer / File(s) Summary
Credential contracts and dependencies
README.md, cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/parameters/*, pom.xml, project/Dependencies.scala, build.sbt, spark-cobol/pom.xml
Reader parameters now include an optional private key and passphrase. Bouncy Castle dependencies and assembly shading are configured. README usage and option details are documented.
GPG stream decryption
spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/utils/GpgUtils.scala, spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/streaming/*
GpgUtils parses key rings and decrypts literal data. FileStreamer and BufferedFSDataInputStream pass credentials, handle decrypted streams, preserve compression behavior, and close raw and processed streams.
Reader and index propagation
spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/SparkCobolProcessor.scala, spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/*
GPG credentials flow through record extraction, source selection, index creation, compatibility checks, and variable-length scans.
Validation and fixtures
spark-cobol/src/test/*, data/test41_*
Unit and integration tests cover GPG decryption and encrypted COBOL loading. Test keys, encrypted files, copybook data, and expected records are added.
Group-field message cleanup
cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/parser/Copybook.scala, cobol-parser/src/test/scala/za/co/absa/cobrix/cobol/parser/extract/BinaryExtractorSpec.scala
Corrects the GROUP-field error message from “it's” to “its” and updates the matching assertion.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SparkReader
  participant FileStreamer
  participant BufferedFSDataInputStream
  participant GpgUtils
  participant BouncyCastle
  SparkReader->>FileStreamer: open input with GPG credentials
  FileStreamer->>BufferedFSDataInputStream: create stream
  BufferedFSDataInputStream->>GpgUtils: decrypt encrypted input
  GpgUtils->>BouncyCastle: parse key ring and decrypt message
  BouncyCastle-->>GpgUtils: provide literal data stream
  GpgUtils-->>BufferedFSDataInputStream: return decrypted stream
  BufferedFSDataInputStream-->>SparkReader: provide readable data
Loading

Possibly related PRs

  • AbsaOSS/cobrix#810: Overlaps in FileStreamer and BufferedFSDataInputStream compressed-input handling.

Poem

“A key in my paw, encrypted files yield,
GPG streams open across the field.
Bouncy Castle guards each byte,
Spark reads records clean and right.
Hop, hop—tests confirm the flight!”

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes unrelated corrections to the GROUP-field exception message and its test expectation. Move the exception-message corrections to a separate pull request, or explain why they are required for this feature.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: transparent GPG/PGP decryption during file reading.
Linked Issues check ✅ Passed The changes implement transparent PGP decryption from supplied key content and passphrase without requiring in-place file decryption [#757].
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/757-gpg-decryption

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/scanners/CobolScanners.scala (1)

75-84: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Calculate GPG ranges from decrypted bytes.

When gpgPrivateKey is set and fileEndOffset is non-zero, FileStatus.getLen returns the encrypted file length. FileStreamer applies maximumFileBytes after decryption. Ciphertext and plaintext lengths differ, so this path can scan the wrong record range.

Calculate the limit from the decrypted stream length. Otherwise, reject file_end_offset for GPG input.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/scanners/CobolScanners.scala`
around lines 75 - 84, Update the file-size calculation in the scanner flow
before constructing the two FileStreamer instances so GPG input derives
maximumFileBytes from the decrypted stream length, not FileStatus.getLen; when a
decrypted length cannot be obtained, reject non-zero fileEndOffset for GPG input
instead of scanning an incorrect range.
🧹 Nitpick comments (2)
spark-cobol/src/test/scala/za/co/absa/cobrix/spark/cobol/source/integration/Test45PgpEncryptedFilesSpec.scala (1)

39-79: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Add an encrypted variable-length indexed-read test.

This suite tests fixed-record input and disables indexes in its second case. It does not execute the changed GPG paths in IndexBuilder and CobolScanners.

Add an encrypted variable-length fixture with indexes enabled. Assert that indexed and non-indexed reads return the same rows.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@spark-cobol/src/test/scala/za/co/absa/cobrix/spark/cobol/source/integration/Test45PgpEncryptedFilesSpec.scala`
around lines 39 - 79, Add a third test under the “gpg encrypted files” suite
that reads an encrypted variable-length fixture with indexes enabled, then reads
the same fixture with indexes disabled and asserts both results contain
identical rows. Reuse the existing key-loading and Spark read configuration
patterns, and add or reference the corresponding variable-length fixture and
expected data needed by the test.
cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/parameters/CobolParameters.scala (1)

89-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

GPG fields are inserted mid-signature in two public case classes. Both classes belong to the published cobol-parser artifact, and both place the new fields after isUtf16BigEndian. Positional construction by downstream code breaks in both cases. The shared fix is to append the fields at the end of each parameter list.

  • cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/parameters/CobolParameters.scala#L89-L90: move gpgPrivateKey and gpgPrivateKeyPassphrase to the end of the parameter list and give them = None defaults, then update the named arguments in CobolParametersParser.parse.
  • cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/parameters/ReaderParameters.scala#L100-L101: move the two fields to the end of the parameter list, keeping the existing = None defaults.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/parameters/CobolParameters.scala`
around lines 89 - 90, Preserve positional-construction compatibility by moving
gpgPrivateKey and gpgPrivateKeyPassphrase to the end of both public case-class
parameter lists: in
cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/parameters/CobolParameters.scala:89-90,
add = None defaults and update named arguments in CobolParametersParser.parse;
in
cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/parameters/ReaderParameters.scala:100-101,
move the fields while retaining their existing defaults.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@spark-cobol/pom.xml`:
- Around line 52-57: Add an org.bouncycastle.** relocation rule to the root
POM’s maven-shade-plugin configuration, alongside the existing execution
settings, matching the relocation already defined in build.sbt. Ensure the
shaded spark-cobol artifact rewrites all BouncyCastle packages without changing
the dependency declaration.

In
`@spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/streaming/BufferedFSDataInputStream.scala`:
- Around line 131-146: Retain the raw FSDataInputStream opened in the GPG branch
of BufferedFSDataInputStream, including when GpgUtils.decryptStream throws, and
update close() to close that raw stream after closing the decrypted input stream
when it is non-null. Keep the existing compressed and uncompressed stream
handling unchanged.
- Around line 148-150: Update the compressed/non-seekable branch in
BufferedFSDataInputStream’s start-offset handling to advance by repeatedly
reading into a scratch buffer, rather than calling baseStream.skip. Continue
until the requested offset is consumed or read() returns -1, ensuring zero-byte
skip behavior cannot prematurely stop FileStreamer.next with fewer bytes.

In
`@spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/utils/GpgUtils.scala`:
- Around line 81-87: Update GpgUtils.scala in the bcProvider initialization to
reuse the registered provider only when it is an instance of the relocated
BouncyCastleProvider; otherwise create and use a private unregistered instance.
Keep build.sbt lines 153-154 unchanged, as the existing relocation rule requires
no direct modification.

---

Outside diff comments:
In
`@spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/scanners/CobolScanners.scala`:
- Around line 75-84: Update the file-size calculation in the scanner flow before
constructing the two FileStreamer instances so GPG input derives
maximumFileBytes from the decrypted stream length, not FileStatus.getLen; when a
decrypted length cannot be obtained, reject non-zero fileEndOffset for GPG input
instead of scanning an incorrect range.

---

Nitpick comments:
In
`@cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/parameters/CobolParameters.scala`:
- Around line 89-90: Preserve positional-construction compatibility by moving
gpgPrivateKey and gpgPrivateKeyPassphrase to the end of both public case-class
parameter lists: in
cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/parameters/CobolParameters.scala:89-90,
add = None defaults and update named arguments in CobolParametersParser.parse;
in
cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/parameters/ReaderParameters.scala:100-101,
move the fields while retaining their existing defaults.

In
`@spark-cobol/src/test/scala/za/co/absa/cobrix/spark/cobol/source/integration/Test45PgpEncryptedFilesSpec.scala`:
- Around line 39-79: Add a third test under the “gpg encrypted files” suite that
reads an encrypted variable-length fixture with indexes enabled, then reads the
same fixture with indexes disabled and asserts both results contain identical
rows. Reuse the existing key-loading and Spark read configuration patterns, and
add or reference the corresponding variable-length fixture and expected data
needed by the test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6bf65c85-d123-4818-abc1-a1174c6aea38

📥 Commits

Reviewing files that changed from the base of the PR and between 40433de and c26f765.

📒 Files selected for processing (18)
  • README.md
  • build.sbt
  • cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/parameters/CobolParameters.scala
  • cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/parameters/CobolParametersParser.scala
  • cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/parameters/ReaderParameters.scala
  • pom.xml
  • project/Dependencies.scala
  • spark-cobol/pom.xml
  • spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/SparkCobolProcessor.scala
  • spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/DefaultSource.scala
  • spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/index/IndexBuilder.scala
  • spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/scanners/CobolScanners.scala
  • spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/streaming/BufferedFSDataInputStream.scala
  • spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/streaming/FileStreamer.scala
  • spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/utils/GpgUtils.scala
  • spark-cobol/src/test/resources/test/test_gpg_key.asc
  • spark-cobol/src/test/scala/za/co/absa/cobrix/spark/cobol/source/integration/Test45PgpEncryptedFilesSpec.scala
  • spark-cobol/src/test/scala/za/co/absa/cobrix/spark/cobol/utils/GpgUtilsSuite.scala

Comment thread spark-cobol/pom.xml
Comment on lines +52 to +57
<!-- GPG support -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpg-jdk18on</artifactId>
</dependency>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect shade/relocation configuration in both POMs.
fd -t f 'pom.xml' | while IFS= read -r f; do
  echo "=== $f ==="
  rg -n -C8 'maven-shade-plugin|<relocation>|<relocations>|shadedPattern|bouncycastle' "$f"
done

Repository: AbsaOSS/cobrix

Length of output: 3790


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== root pom relevant sections ==="
sed -n '90,220p' pom.xml
sed -n '350,520p' pom.xml

echo "=== spark-cobol pom relevant sections ==="
sed -n '1,120p' spark-cobol/pom.xml

echo "=== build.sbt relevant shading lines ==="
sed -n '140,165p' build.sbt

echo "=== all occurrences of bouncycastle relocate-like references ==="
rg -n -i 'relocate|relocations|bouncycastle|bcpg-jdk18on|bcprov' . -g '!target' -g '!\.git' -g '!out'

Repository: AbsaOSS/cobrix

Length of output: 13347


Add BouncyCastle relocation to the Maven shade configuration.

build.sbt relocates org.bouncycastle.**, but the root POM’s maven-shade-plugin configuration only has <execution> and no <relocations>. Add the same BouncyCastle relocation for Maven so the released spark-cobol jar does not expose BouncyCastle classes that can collide with the cluster classpath.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@spark-cobol/pom.xml` around lines 52 - 57, Add an org.bouncycastle.**
relocation rule to the root POM’s maven-shade-plugin configuration, alongside
the existing execution settings, matching the relocation already defined in
build.sbt. Ensure the shaded spark-cobol artifact rewrites all BouncyCastle
packages without changing the dependency declaration.

Comment on lines +148 to +150
if (startOffset > 0) {
if (codec == null) {
fsIn.seek(startOffset)
if (!isCompressedStream) {
baseStream.asInstanceOf[FSDataInputStream].seek(startOffset)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate all callers that construct streams with a non-zero start offset for encrypted input.
rg -nP --type=scala -C4 'new FileStreamer\(' 
rg -nP --type=scala -C4 'def getStreams\('

Repository: AbsaOSS/cobrix

Length of output: 152


🏁 Script executed:

#!/bin/bash
set -u

echo "== find file =="
fd -a 'BufferedFSDataInputStream\.scala$' . || true

echo "== git status/diff stat =="
git status --short || true
git diff --stat || true

echo "== search for BufferFS / FileStreamer / startOffset / isCompressedStream =="
rg -n -C 3 'BufferedFSDataInputStream|FileStreamer|startOffset|isCompressedStream|skip(' . -S || true

Repository: AbsaOSS/cobrix

Length of output: 531


🏁 Script executed:

#!/bin/bash
set -u

file="$(fd 'BufferedFSDataInputStream\.scala$' . | head -n 1 || true)"
if [ -n "$file" ]; then
  echo "== line count and outline =="
  wc -l "$file"
  ast-grep outline "$file" || true
  echo "== relevant lines 1-240 =="
  sed -n '1,240p' "$file" | nl -ba | sed -n '1,240p'
fi

Repository: AbsaOSS/cobrix

Length of output: 479


🏁 Script executed:

#!/bin/bash
set -u
file="$(fd 'BufferedFSDataInputStream\.scala$' . | head -n 1)"
echo "== file =="
printf '%s\n' "$file"
echo "== lines =="
awk '{printf "%5d\t%s\n", NR, $0}' "$file"
echo "== searches =="
rg -n -C 3 'BufferedFSDataInputStream|FileStreamer|startOffset|isCompressedStream|skip\(' . -S || true

Repository: AbsaOSS/cobrix

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -u

echo "== FileStreamer definitions/usages =="
rg -n -C 4 'class FileStreamer|object FileStreamer|new FileStreamer\s*\(|def FileStreamer\s*\(' spark-cobol cobol-parser -S

echo "== GpgUtils.decryptStream signature/usages =="
fd -a 'GpgUtils\.scala$' . | xargs -r sed -n '1,220p'

echo "== stream wrapper/read stream behavior around buffered stream =="
sed -n '1,280p' spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/SparkCobolProcessor.scala | awk '{printf "%5d\t%s\n", NR, $0}'

echo "== readFully/read stream consumer candidates =="
rg -n -C 3 'readFully\s*\(|next\r?\(|BufferedFSDataInputStream|readFullyHelper' spark-cobol/src/main/scala -S

Repository: AbsaOSS/cobrix

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -u

echo "== FileStreamer.scala =="
awk '{printf "%5d\t%s\n", NR, $0}' spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/streaming/FileStreamer.scala

echo "== exact non-test non-deprecation non-log skip/readFully search =="
rg -n -C 4 'def\s+(next|\bread\b|readFully|available|close|size)|readFully\s*\(|next\s*\(|skip\s*\(|new\s+BufferedFSDataInputStream' spark-cobol/src/main/scala spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source -S

echo "== deterministic InputStream.skip contract probe from installed OpenJDK docs/source if available =="
java -version 2>&1 | head -5 || true

Repository: AbsaOSS/cobrix

Length of output: 47155


🏁 Script executed:

#!/bin/bash
set -u

python3 - <<'PY'
from pathlib import Path
p = Path('spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/streaming/BufferedFSDataInputStream.scala')
src = p.read_text()
skip_block = src[src.find('while (toSkip > 0)'):src.find('}')+1]
print('skip_block_contains_return_to_skip_zero:', return_to_skip_zero := 'if (skipped <= 0) return baseStream' in skip_block)
print('skip_block_contains_only_skip_loop:', all(s in skip_block for s in ('baseStream.skip(toSkip)', 'toSkip -= skipped')))
# Simulate the loop with a deterministic "skip returns 0 but stream remains".
def simulated_skip_stream(total):
    skipped = 0
    while skipped < total:
        skipped0 = 0
        if skipped0 <= 0:
            return skipped
        skipped += skipped0
    return skipped

print('simulated_skip_with_return_zero_returns_skipped:', simulated_skip_stream(123))
print('simulated_skip_reading_until_target_returns_target', 'read scratch loop should return target if stream remains')
PY

echo "== Java InputStream.skip Javadoc on installed JDK, if available =="
find "$JAVA_HOME" "$JDK_HOME" /usr/lib/jvm -maxdepth 2 \( -name '*.html' -o -name '*.src' \) 2>/dev/null | xargs -r grep -l 'skip(long n)' | head -5 | while read -r f; do
  printf '\nFILE: %s\n' "$f"
  grep -n -C 3 'Returns:|skipped|InputStream.skip' "$f" | head -40
done | head -120 || true

Repository: AbsaOSS/cobrix

Length of output: 509


Skip by reading instead of relying on InputStream.skip.

In non-seekable compressed/PGP streams, baseStream.skip(toSkip) can return 0 while data remains; the loop then stops and FileStreamer.next can return fewer bytes than requested without an error. Replace this loop with a read-scratch approach and only stop on read() returning -1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/streaming/BufferedFSDataInputStream.scala`
around lines 148 - 150, Update the compressed/non-seekable branch in
BufferedFSDataInputStream’s start-offset handling to advance by repeatedly
reading into a scratch buffer, rather than calling baseStream.skip. Continue
until the requested offset is consumed or read() returns -1, ensuring zero-byte
skip behavior cannot prematurely stop FileStreamer.next with fewer bytes.

…o enable transparent decryption of GPG-encrypted files.
@yruslan
yruslan force-pushed the feature/757-gpg-decryption branch from c26f765 to 64fa1d7 Compare August 4, 2026 08:44

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Around line 1699-1707: Change the “File reading options” subsection heading in
the README from level five to level three, using “### File reading options”
beneath “## Summary of all available options” to maintain the required heading
hierarchy.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 75d47797-54b7-4a27-96d3-a59994785c35

📥 Commits

Reviewing files that changed from the base of the PR and between c26f765 and 64fa1d7.

📒 Files selected for processing (18)
  • README.md
  • cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/parameters/CobolParameters.scala
  • cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/parameters/CobolParametersParser.scala
  • cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/parameters/ReaderParameters.scala
  • data/test41_copybook.cob
  • data/test41_data/example.bin.gpg
  • data/test41_expected/test41a.txt
  • data/test41_expected/test41b.txt
  • spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/SparkCobolProcessor.scala
  • spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/DefaultSource.scala
  • spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/index/IndexBuilder.scala
  • spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/scanners/CobolScanners.scala
  • spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/streaming/BufferedFSDataInputStream.scala
  • spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/streaming/FileStreamer.scala
  • spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/utils/GpgUtils.scala
  • spark-cobol/src/test/resources/test/test_gpg_file.gpg
  • spark-cobol/src/test/scala/za/co/absa/cobrix/spark/cobol/source/integration/Test45PgpEncryptedFilesSpec.scala
  • spark-cobol/src/test/scala/za/co/absa/cobrix/spark/cobol/utils/GpgUtilsSuite.scala
🚧 Files skipped from review as they are similar to previous changes (9)
  • spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/SparkCobolProcessor.scala
  • cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/parameters/CobolParameters.scala
  • cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/parameters/CobolParametersParser.scala
  • cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/reader/parameters/ReaderParameters.scala
  • spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/utils/GpgUtils.scala
  • spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/streaming/FileStreamer.scala
  • spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/index/IndexBuilder.scala
  • spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/DefaultSource.scala
  • spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/scanners/CobolScanners.scala

Comment thread README.md
Comment on lines +1699 to +1707
| Option (usage example) | Description |
|-----------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------|
| .option("data_paths", "/path1,/path2") | Allows loading data from multiple unrelated paths on the same filesystem. |
| .option("file_start_offset", "0") | Specifies the number of bytes to skip at the beginning of each file. |
| .option("file_end_offset", "0") | Specifies the number of bytes to skip at the end of each file. |
| .option("record_start_offset", "0") | Specifies the number of bytes to skip at the beginning of each record before applying copybook fields to data. |
| .option("record_end_offset", "0") | Specifies the number of bytes to skip at the end of each record after applying copybook fields to data. |
| .option("gpg_private_key", ascGpgKeyContent) | Specifies the ASCII-armored GPG private key for decrypting data files. |
| .option("gpg_private_key_passphrase", "passphrase") | Specifies the passphrase for the ASCII-armored GPG private key used for decrypting data files. If not specified, empty passphrase will be used. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use an h3 heading for this subsection.

Line [1697] jumps from ## Summary of all available options to ##### File reading options. Change it to ### File reading options so the document outline complies with markdownlint MD001.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 1699 - 1707, Change the “File reading options”
subsection heading in the README from level five to level three, using “### File
reading options” beneath “## Summary of all available options” to maintain the
required heading hierarchy.

Source: Linters/SAST tools

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

JaCoCo code coverage report - 'cobol-parser'

Overall Project 90.08% 🍏
Files changed 100% 🍏

File Coverage
Copybook.scala 71.86% 🍏

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

JaCoCo code coverage report - 'spark-cobol'

Overall Project 83.4% -0.68% 🍏
Files changed 88.17% 🍏

File Coverage
FileStreamer.scala 99.11% 🍏
IndexBuilder.scala 96.64% -0.7% 🍏
SparkCobolProcessor.scala 96.46% 🍏
CobolScanners.scala 93.36% 🍏
GpgUtils.scala 87.91% -13.49% 🍏
BufferedFSDataInputStream.scala 87.72% -7.6%
DefaultSource.scala 83.74% 🍏

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/streaming/BufferedFSDataInputStream.scala`:
- Around line 143-155: Update the GPG initialization flow around openStream() so
rawStream remains stored on the instance after fileSystem.open(filePath),
including when post-open operations such as BaseStream.skip fail. Add a
dedicated constructor-initialization cleanup path that closes this stored
rawStream when any subsequent initialization throws, while preserving the
existing decryptStream failure cleanup.
- Around line 51-62: Update close() in BufferedFSDataInputStream so in.close()
is attempted before rawStream.close(), with each close wrapped in its own
independent Try to ensure both are attempted even if the first fails. Keep
clearing in and rawStream after the close attempts, including when the decrypted
stream close fails.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b422fece-4707-495f-a35a-7809d446c6e7

📥 Commits

Reviewing files that changed from the base of the PR and between e4a699a and 1d1332c.

📒 Files selected for processing (3)
  • cobol-parser/src/main/scala/za/co/absa/cobrix/cobol/parser/Copybook.scala
  • cobol-parser/src/test/scala/za/co/absa/cobrix/cobol/parser/extract/BinaryExtractorSpec.scala
  • spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/streaming/BufferedFSDataInputStream.scala

Comment on lines +143 to +155
rawStream = fileSystem.open(filePath)
try {
GpgUtils.decryptStream(rawStream, keyring, gpgPassphrase.map(_.toCharArray).getOrElse(Array.emptyCharArray))
} catch {
case ex: Throwable =>
// Close rawStream only if decryptStream() fails to return a decrypted stream. Ignore errors that might happen on close.
Try {
if (rawStream != null) {
rawStream.close()
}
}
rawStream = null
throw ex

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the target file and related GpgUtils call sites.
target="spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/streaming/BufferedFSDataInputStream.scala"
if [ -f "$target" ]; then
  echo "== target exists =="
  wc -l "$target"
  echo "== outline =="
  ast-grep outline "$target" || true
  echo "== relevant lines 1-230 =="
  sed -n '1,230p' "$target" | nl -ba
else
  echo "target not found: $target"
  echo "candidate files:"
  fd -i 'BufferedFSDataInputStream\.scala|GpgUtils' .
fi

echo "== DecryptStream references =="
rg -n "GpgUtils\.decryptStream|decryptStream\(|BufferedFSDataInputStream|class BufferedFSDataInputStream|def close\(|startOffset|skip|baseStream|rawStream" spark-cobol/src/main/scala -S || true

echo "== Other close patterns =="
rg -n "rawStream\s*=|baseStream\s*=|startOffset|GpgUtils\.decryptStream" spark-cobol/src/main/scala -S -C 3 || true

Repository: AbsaOSS/cobrix

Length of output: 484


🏁 Script executed:

#!/bin/bash
set -euo pipefail

target="spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/streaming/BufferedFSDataInputStream.scala"

echo "== target lines =="
awk '{printf "%6d  %s\n", NR, $0}' "$target"

echo "== decrypt/close/startSkip/openStream references =="
rg -n "GpgUtils\.decryptStream|decryptStream\(|BufferedFSDataInputStream|def close\(|startOffset|skip|baseStream|rawStream|openStream|new BufferedFSDataInputStream|case.*GpgUtils|GpgUtils" spark-cobol/src/main/scala -S -C 2

echo "== file list for GpgUtils =="
git ls-files | sed -n '/GpgUtils/p'

Repository: AbsaOSS/cobrix

Length of output: 41274


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== GpgUtils.scala relevant implementation =="
awk '{printf "%6d  %s\n", NR, $0}' spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/utils/GpgUtils.scala | sed -n '1,120p'

echo "== FileReader / getStreams call site =="
awk '{printf "%6d  %s\n", NR, $0}' spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/index/IndexBuilder.scala | sed -n '220,275p'

Repository: AbsaOSS/cobrix

Length of output: 10120


Close the opened stream when constructor initialization throws.

rawStream := fileSystem.open(filePath) runs before BaseStream.skip for compressed GPG streams. If startOffset > 0 and skip throws, openStream() returns without assigning in, object construction exits, and close() is called on an object with rawStream == null. Keep rawStream on an instance field for the GPG branch and close it from a dedicated cleanup code path when any post-open initialization throws.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/streaming/BufferedFSDataInputStream.scala`
around lines 143 - 155, Update the GPG initialization flow around openStream()
so rawStream remains stored on the instance after fileSystem.open(filePath),
including when post-open operations such as BaseStream.skip fail. Add a
dedicated constructor-initialization cleanup path that closes this stored
rawStream when any subsequent initialization throws, while preserving the
existing decryptStream failure cleanup.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
spark-cobol/src/test/scala/za/co/absa/cobrix/spark/cobol/source/integration/Test45GpgEncryptedFilesSpec.scala (1)

80-89: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Enable the index path explicitly.

The scenario name says "with indexes", but this block does not set enable_indexes to "true". It relies on the reader default. Add the explicit option, or verify that input_split_records always enables indexing.

Proposed option
       .option("generate_record_id", "true")
+      .option("enable_indexes", "true")
       .option("input_split_records", 1)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@spark-cobol/src/test/scala/za/co/absa/cobrix/spark/cobol/source/integration/Test45GpgEncryptedFilesSpec.scala`
around lines 80 - 89, Update the test case “load normally a
variable-record-length file with indexes” to explicitly set the reader’s
enable_indexes option to "true" alongside the existing input_split_records
option, ensuring the scenario directly exercises indexed reading rather than
relying on defaults.
data/test41_expected/test41c.txt (1)

1-10: 🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy lift

Make the fixture exercise variable record lengths.

Every expected row has Record_Byte_Length set to 2202. The integration scenario is named "load normally a variable-record-length file with indexes". A fixed 2202-byte reader could therefore satisfy this expected output.

Add records with different physical lengths and regenerate data/test41_data/example.bin.gpg and data/test41_expected/test41c.txt. If Record_Byte_Length is not the physical input length for this format, add a direct assertion that the encrypted fixture has varying record boundaries.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@data/test41_expected/test41c.txt` around lines 1 - 10, Update the test41
fixture scenario so the encrypted input contains records with genuinely
different physical lengths, then regenerate example.bin.gpg and test41c.txt so
Record_Byte_Length reflects those lengths instead of always being 2202. If
Record_Byte_Length does not represent physical input size, add a direct
assertion in the variable-record-length integration test that the encrypted
fixture has varying record boundaries.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@data/test41_expected/test41c.txt`:
- Around line 1-10: Update the test41 fixture scenario so the encrypted input
contains records with genuinely different physical lengths, then regenerate
example.bin.gpg and test41c.txt so Record_Byte_Length reflects those lengths
instead of always being 2202. If Record_Byte_Length does not represent physical
input size, add a direct assertion in the variable-record-length integration
test that the encrypted fixture has varying record boundaries.

In
`@spark-cobol/src/test/scala/za/co/absa/cobrix/spark/cobol/source/integration/Test45GpgEncryptedFilesSpec.scala`:
- Around line 80-89: Update the test case “load normally a
variable-record-length file with indexes” to explicitly set the reader’s
enable_indexes option to "true" alongside the existing input_split_records
option, ensuring the scenario directly exercises indexed reading rather than
relying on defaults.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1167e1f0-70e5-4abf-8af2-f0eb3f90ab3e

📥 Commits

Reviewing files that changed from the base of the PR and between 1d1332c and b799427.

📒 Files selected for processing (3)
  • data/test41_expected/test41c.txt
  • spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/streaming/BufferedFSDataInputStream.scala
  • spark-cobol/src/test/scala/za/co/absa/cobrix/spark/cobol/source/integration/Test45GpgEncryptedFilesSpec.scala
🚧 Files skipped from review as they are similar to previous changes (1)
  • spark-cobol/src/main/scala/za/co/absa/cobrix/spark/cobol/source/streaming/BufferedFSDataInputStream.scala

@yruslan
yruslan merged commit 8c87e75 into master Aug 5, 2026
7 checks passed
@yruslan
yruslan deleted the feature/757-gpg-decryption branch August 5, 2026 09:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Transparent PGP Encryption

1 participant