Skip to content

feat: add markdown/xml export selector with hardened XML output#40

Merged
Mehdi-Bl merged 16 commits into
mainfrom
feat/xml-export-hardening
Feb 9, 2026
Merged

feat: add markdown/xml export selector with hardened XML output#40
Mehdi-Bl merged 16 commits into
mainfrom
feat/xml-export-hardening

Conversation

@Mehdi-Bl

@Mehdi-Bl Mehdi-Bl commented Feb 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add export format support (/) through config, renderer, IPC, and main processing
  • harden XML generation: shared helpers, attribute escaping, invalid XML char sanitization, and safe CDATA wrapping
  • align token accounting so per-file XML token attributes and summary totals use the same normalized source
  • improve save behavior so default filename extension and dialog filters match selected export format

Tests

  • added integration test for real XML export path through main handler + content processor:

  • added utility unit tests:

  • expanded coverage for XML edge cases and save dialog filter ordering:

Validation run

ai-code-fusion@0.2.0 lint
cross-env ESLINT_USE_FLAT_CONFIG=false eslint src tests --ext .js,.jsx,.ts,.tsx --cache

ai-code-fusion@0.2.0 test
jest --config jest.config.js --passWithNoTests --runInBand

  • trim trailing whitespace.............................(no files to check)Skipped
    fix end of files.....................................(no files to check)Skipped
    check yaml...........................................(no files to check)Skipped
    check json...........................................(no files to check)Skipped
    mixed line ending....................................(no files to check)Skipped
    eslint...............................................(no files to check)Skipped
    prettier.............................................(no files to check)Skipped
    Detect hardcoded secrets.............................(no files to check)Skipped

Notes on prior review feedback

  • addressed previous review concerns around duplicated XML helpers, token consistency drift, and missing XML edge-case coverage in this change set.

Summary by Sourcery

Add configurable export format support with XML output, integrate it through main processing, renderer, and config, and harden XML generation and token accounting.

New Features:

  • Introduce markdown vs XML export format selection via configuration and UI, wiring it through IPC and repository processing to control output format and default save extension.
  • Support XML repository export that wraps content and file listings in a structured XML document with per-file metadata and summary attributes.

Bug Fixes:

  • Align token counting and path normalization across analysis, processing, and token-counting handlers to ensure consistent totals and safe path handling.

Enhancements:

  • Harden XML output with shared helpers for attribute escaping, CDATA wrapping, and invalid character sanitization to produce well-formed XML even for edge-case content.
  • Improve save dialog behavior so file-type filters and default extensions prioritize the currently selected export format.

Tests:

  • Add end-to-end and integration tests for XML export through the main process, including binary file handling, summary attributes, and tree view inclusion.
  • Extend unit tests for the content processor, export-format utilities, app component, and config tab to cover XML formatting, configuration persistence, and save behavior.
  • Update YAML mock and config fixtures to support the new export_format option and related processing flags.

Summary by CodeRabbit

  • New Features

    • XML export added alongside Markdown; Export Format selector in settings and optional tree view in exports.
  • Improvements

    • Exports include richer file/directory metadata, optional per-file token counts, and adaptive save dialog default extension.
    • Processing honors selected files and export format consistently.
  • Reliability

    • Stronger path/root validation, better binary-file handling, centralized error reporting, and more robust traversal/filtering.
  • Tests

    • New unit and integration tests covering XML export, sanitization, token counting, and root-path safety.

@sourcery-ai

sourcery-ai Bot commented Feb 9, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds configurable markdown/XML export support end-to-end (config → renderer → IPC → main), introduces hardened XML generation utilities (escaping, sanitization, CDATA wrapping, numeric attribute normalization), aligns token accounting between per‑file and summary XML, and updates save behavior so dialog defaults and filters track the chosen export format, with integration and unit tests for XML output and save dialog behavior.

Sequence diagram for configurable markdown/XML export flow

sequenceDiagram
  actor User
  participant ConfigTab
  participant App
  participant ElectronIPC as ElectronIPC
  participant MainProcess as MainProcess_index
  participant ContentProcessor
  participant FileSystem

  User->>ConfigTab: Select exportFormat
  ConfigTab->>ConfigTab: Update export_format in configContent
  ConfigTab-->>App: onConfigChange(configContent)

  User->>App: Click Analyze and Process
  App->>ElectronIPC: invoke repo:process(rootPath, filesInfo, treeView, options)
  note over App: options include exportFormat
  ElectronIPC->>MainProcess: repo:process handler

  MainProcess->>MainProcess: normalizeExportFormat(options.exportFormat)
  MainProcess->>ContentProcessor: new ContentProcessor(tokenCounter)
  loop For each fileInfo
    MainProcess->>ContentProcessor: processFile(filePath, relativePath, options)
    alt exportFormat is xml
      ContentProcessor->>ContentProcessor: isBinaryFile(filePath)
      alt binary
        ContentProcessor->>ContentProcessor: escapeXmlAttribute, wrapXmlCdata
        ContentProcessor-->>MainProcess: <file binary="true" ...>
      else text
        ContentProcessor->>FileSystem: readFileSync(filePath)
        ContentProcessor->>ContentProcessor: normalizeTokenCount(tokenCount)
        ContentProcessor->>ContentProcessor: wrapXmlCdata(content)
        ContentProcessor-->>MainProcess: <file tokens="..."> <![CDATA[...]]> </file>
      end
    else exportFormat is markdown
      ContentProcessor->>FileSystem: readFileSync(filePath)
      ContentProcessor-->>MainProcess: Markdown section
    end
    MainProcess->>MainProcess: Accumulate totalTokens, processedFiles, skippedFiles
  end

  alt exportFormat is xml
    MainProcess->>MainProcess: toXmlNumericAttribute for summary attributes
    MainProcess-->>ElectronIPC: XML repositoryContent with summary
  else markdown
    MainProcess-->>ElectronIPC: Markdown content with --END--
  end
  ElectronIPC-->>App: ProcessRepositoryResult

  User->>App: Click Save Output
  App->>App: resolveExportFormatFromConfig(configContent)
  App->>ElectronIPC: saveFile(content, defaultPath with extension)
  ElectronIPC->>MainProcess: fs:saveFile handler
  MainProcess->>MainProcess: Compute filters based on defaultPath extension
  MainProcess->>FileSystem: writeFile(filePath, content)
  MainProcess-->>ElectronIPC: Save success
  ElectronIPC-->>App: Save completed
Loading

Updated class diagram for ContentProcessor and export format utilities

classDiagram
  class ContentProcessor {
    - tokenCounter TokenCounter
    + ContentProcessor(tokenCounter TokenCounter)
    + processFile(filePath string, relativePath string, options ProcessFileOptions) string
  }

  class ProcessFileOptions {
    + exportFormat ExportFormat
    + showTokenCount boolean
    + tokenCount number
  }

  class ExportFormatUtil {
    <<utility>>
    + normalizeExportFormat(format unknown) ExportFormat
    + escapeXmlAttribute(value string) string
    + sanitizeXmlContent(value string) string
    + wrapXmlCdata(value string) string
    + normalizeTokenCount(value unknown) number
    + toXmlNumericAttribute(value unknown) string
  }

  class TokenCounter {
    + countTokens(content string) number
  }

  class ConfigObject {
    + export_format ExportFormat
    + show_token_count boolean
    + include_tree_view boolean
    + exclude_suspicious_files boolean
  }

  class ProcessRepositoryOptions {
    + rootPath string
    + filesInfo FileInfo[]
    + treeView string
    + options ProcessRepositoryInnerOptions
  }

  class ProcessRepositoryInnerOptions {
    + showTokenCount boolean
    + includeTreeView boolean
    + exportFormat ExportFormat
  }

  class FileInfo {
    + path string
    + tokens number
    + isBinary boolean
  }

  class AppComponent {
    + processingOptions ProcessingOptions
    + resolveExportFormatFromConfig(rawConfigContent string) ExportFormat
  }

  class ProcessingOptions {
    + showTokenCount boolean
    + includeTreeView boolean
    + exportFormat ExportFormat
  }

  class ConfigTabComponent {
    + exportFormat ExportFormat
    + updateConfigStates(config ConfigObject, stateSetters ConfigStateSetters) void
  }

  class ConfigStateSetters {
    + setExportFormat(exportFormat ExportFormat) void
  }

  class MainProcess_index {
    + repo_process(options ProcessRepositoryOptions) ProcessRepositoryResult
    + fs_saveFile(content string, defaultPath string) void
    + getErrorMessage(error unknown) string
  }

  class ProcessRepositoryResult {
    + content string
    + totalTokens number
    + processedFiles number
    + skippedFiles number
    + filesInfo FileInfo[]
  }

  class ExportFormat {
    <<enumeration>>
    markdown
    xml
  }

  ContentProcessor --> TokenCounter
  ContentProcessor ..> ProcessFileOptions
  ProcessRepositoryOptions --> ProcessRepositoryInnerOptions
  ProcessRepositoryOptions --> FileInfo
  ProcessRepositoryResult --> FileInfo
  AppComponent --> ProcessingOptions
  AppComponent ..> ExportFormatUtil
  ConfigTabComponent --> ConfigStateSetters
  ConfigTabComponent --> ConfigObject
  ConfigTabComponent ..> ExportFormatUtil
  MainProcess_index ..> ContentProcessor
  MainProcess_index ..> ExportFormatUtil
  MainProcess_index --> ProcessRepositoryOptions
  MainProcess_index --> ProcessRepositoryResult
  ExportFormatUtil ..> ExportFormat
  ConfigObject --> ExportFormat
  ProcessFileOptions --> ExportFormat
  ProcessingOptions --> ExportFormat
Loading

File-Level Changes

Change Details Files
Add export format selection (markdown/xml) to configuration, renderer state, and processing options wiring.
  • Extend ConfigObject and ProcessingOptions with export_format, defaulting to markdown and normalized via normalizeExportFormat.
  • Update ConfigTab to read/write export_format, keep it in React state, and render a dropdown selector persisted to YAML.
  • Ensure App derives processing options (showTokenCount, includeTreeView, exportFormat) from config/localStorage and passes exportFormat through to processRepository and saveFile default path resolution.
src/types/ipc.ts
src/renderer/components/ConfigTab.tsx
src/renderer/components/App.tsx
src/utils/config.default.yaml
tests/fixtures/configs/default.yaml
tests/fixtures/configs/minimal.yaml
tests/unit/components/config-tab.test.tsx
tests/unit/components/app.test.tsx
tests/mocks/yaml-mock.ts
Introduce shared export-format utilities and use them to harden XML attribute/content handling and token normalization.
  • Create export-format utility module with helpers for export format normalization, XML attribute escaping, content sanitization, safe CDATA wrapping, and numeric attribute normalization.
  • Adopt normalizeExportFormat and normalizeTokenCount in main repo:process handler and ContentProcessor to ensure consistent export format and token usage.
  • Expose toXmlNumericAttribute for use in XML summary attributes to ensure safe, numeric XML attribute values.
src/utils/export-format.ts
src/utils/content-processor.ts
src/main/index.ts
tests/unit/utils/export-format.test.ts
tests/unit/utils/content-processor.test.ts
tests/integration/main-process/xml-export-e2e.test.ts
tests/integration/main-process/handlers.test.ts
Extend ContentProcessor and main repo:process handler to support XML output format with hardened structure and metrics.
  • Update ContentProcessor.processFile to accept options (exportFormat, showTokenCount, tokenCount) and emit either markdown or XML based on normalized exportFormat.
  • For text files, emit XML elements with escaped attributes, optional tokens attribute, and body wrapped in safely-split/sanitized CDATA.
  • For binary files, emit XML elements with binary metadata (fileType, sizeKB) and a CDATA note instead of content, while preserving markdown behavior when not in XML mode.
  • Update main repo:process handler to generate XML prolog/root wrapper, optional CDATA section, list, and a element whose attributes use normalized token counts.
  • Ensure non-XML processing still produces the existing markdown header, tree view block, file sections, and --END-- terminator.
src/utils/content-processor.ts
src/main/index.ts
tests/unit/utils/content-processor.test.ts
tests/integration/main-process/handlers.test.ts
tests/integration/main-process/xml-export-e2e.test.ts
Align token accounting and error handling in main process for repository processing and token counting.
  • Normalize per-file tokens from filesInfo using normalizeTokenCount and propagate these into ContentProcessor and XML output rather than trusting raw values.
  • Use a shared getErrorMessage helper in main repo:process handler to avoid unsafe .message access on unknown errors.
  • Preserve and slightly refactor existing analyzeRepository and tokens:countFiles handlers with minimal behavioral change, keeping binary files excluded from token counting and analysis while tightening try/catch blocks.
src/main/index.ts
Adjust save dialog behavior to respect chosen export format and improve tests for dialog usage.
  • Change fs:saveFile IPC handler to compute dialog filters dynamically based on the defaultPath extension, prioritizing XML filters when saving .xml and markdown filters otherwise, and handle nullable mainWindow.
  • Update App.save handler to pick output.md vs output.xml defaultPath by resolving export format from the current YAML config.
  • Extend main-process integration tests to mock dialog.showSaveDialog and verify filter ordering, selected paths, and cancellation behavior.
src/main/index.ts
src/renderer/components/App.tsx
tests/integration/main-process/handlers.test.ts
tests/unit/components/app.test.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions

github-actions Bot commented Feb 9, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@coderabbitai

coderabbitai Bot commented Feb 9, 2026

Copy link
Copy Markdown

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

Walkthrough

Adds XML export support and export-format utilities, extends ContentProcessor to emit XML (attributes + CDATA), updates IPC types/signatures (countFilesTokens payload), centralizes path/config validation in main handlers, updates renderer UI to select export_format, and expands tests/mocks for XML and new IPC shapes.

Changes

Cohort / File(s) Summary
Main process & IPC
src/main/index.ts, src/main/preload.ts, src/types/ipc.ts
Refactored directory/tree analysis and repo handlers; unified exclude/gitignore workflow; added path containment checks; changed countFilesTokens to accept { rootPath, filePaths }; added new IPC types (ExportFormat, CountFilesTokensOptions, updated Process... types).
Export-format utilities
src/utils/export-format.ts
New helpers for export normalization and XML safety: normalizeExportFormat, sanitizeXmlContent, escapeXmlAttribute, wrapXmlCdata, normalizeTokenCount, toXmlNumericAttribute.
Content processing
src/utils/content-processor.ts
processFile(..., options) extended to accept export options; emits XML <file> elements (attributes + CDATA) for xml format; preserves non-XML behavior and binary handling.
Renderer UI & state
src/renderer/components/App.tsx, src/renderer/components/ConfigTab.tsx, src/renderer/components/SourceTab.tsx
Added export format UI/state persisted as export_format; propagate exportFormat in processing options; update save default extension logic; call countFilesTokens with { rootPath, filePaths }.
Config defaults & fixtures
src/utils/config.default.yaml, tests/fixtures/configs/*
Added export_format: markdown to default config and test fixtures.
FS & save dialog
src/main/index.ts (fs:saveFile)
Dialog filters and invocation adapt to detected extension and mainWindow presence; resolves null on cancel.
Token counting & processing flows
src/main/index.ts, src/utils/*
tokens:countFiles now accepts options, validates containment, skips binaries, returns structured results; repo:process/ analyze integrate gitignore and export-format helpers for numeric/XML formatting.
Tests & mocks
tests/..., tests/mocks/yaml-mock.ts, tests/setup.ts
Added unit/integration tests for XML export and export-format utils; updated mocks/tests for IPC payload shape and processFile signature; added xml-export-e2e test.
Catalog & wiring
tests/catalog.md, tests/setup.ts
Registered new tests and updated mocked processRepository result to include exportFormat.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as Renderer (App/ConfigTab/SourceTab)
    participant Main as Main Process
    participant CP as ContentProcessor
    participant FS as File System/Dialog

    User->>UI: Set exportFormat & start processing
    UI->>Main: repo:process({ rootPath, exportFormat, selectedFiles, options })
    Main->>Main: repo:analyze (gather files, apply gitignore/config, validate paths)
    loop per file
        Main->>CP: processFile(path, relPath, { exportFormat, showTokenCount })
        alt exportFormat == "xml"
            CP-->>Main: XML <file> fragment (attributes + CDATA)
        else
            CP-->>Main: Markdown/text fragment
        end
    end
    Main->>Main: Assemble output, summary, token totals
    Main-->>UI: Return result { content, exportFormat, totals, filesInfo }
    UI->>FS: fs:saveFile(defaultPath based on exportFormat)
    FS-->>UI: File written or canceled
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I nibble tags and hop through code,
XML leaves wrapped safe in mode,
CDATA snug where text may roam,
Tokens tallied, files find home.
Hop, export, save — then off I go.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main changes: adding a markdown/xml export format selector and hardening XML output generation.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/xml-export-hardening

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 and usage tips.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Review Summary by Qodo

Add XML export format support with hardened output and comprehensive test coverage

✨ Enhancement 🧪 Tests

Grey Divider

Walkthroughs

Description
• Add XML export format support with hardened output generation
• Implement shared XML utility helpers for safe attribute escaping and CDATA wrapping
• Normalize token counting across file processing and XML attributes
• Extend save dialog filters to prioritize format based on file extension
• Add comprehensive integration and unit tests for XML export end-to-end flow
Diagram
flowchart LR
  Config["Config with export_format"] -->|normalizeExportFormat| Processor["ContentProcessor"]
  Processor -->|escapeXmlAttribute, wrapXmlCdata| XmlOutput["XML Output"]
  Processor -->|normalizeTokenCount| TokenNorm["Normalized Tokens"]
  TokenNorm -->|toXmlNumericAttribute| XmlAttrs["XML Attributes"]
  XmlOutput -->|showSaveDialog with filters| SaveDialog["Save Dialog"]
  SaveDialog -->|prioritize by extension| SaveFile["File Saved"]
Loading

Grey Divider

File Changes

1. src/utils/export-format.ts ✨ Enhancement +49/-0

New XML export utility helpers and normalization functions

src/utils/export-format.ts


2. src/main/index.ts ✨ Enhancement +327/-274

Integrate XML export format and update save dialog filters

src/main/index.ts


3. src/types/ipc.ts ✨ Enhancement +3/-0

Add ExportFormat type and export_format config option

src/types/ipc.ts


View more (13)
4. src/utils/content-processor.ts ✨ Enhancement +49/-1

Support XML output format with proper escaping and CDATA wrapping

src/utils/content-processor.ts


5. src/renderer/components/App.tsx ✨ Enhancement +25/-6

Resolve export format from config and use in save path

src/renderer/components/App.tsx


6. src/renderer/components/ConfigTab.tsx ✨ Enhancement +31/-1

Add export format selector dropdown to configuration UI

src/renderer/components/ConfigTab.tsx


7. src/utils/config.default.yaml ⚙️ Configuration changes +1/-0

Add default export_format configuration value

src/utils/config.default.yaml


8. tests/fixtures/configs/default.yaml ⚙️ Configuration changes +1/-0

Add export_format to default test configuration

tests/fixtures/configs/default.yaml


9. tests/fixtures/configs/minimal.yaml ⚙️ Configuration changes +1/-0

Add export_format to minimal test configuration

tests/fixtures/configs/minimal.yaml


10. tests/unit/utils/export-format.test.ts 🧪 Tests +42/-0

New unit tests for XML utility functions and normalization

tests/unit/utils/export-format.test.ts


11. tests/unit/utils/content-processor.test.ts 🧪 Tests +56/-0

Add tests for XML file processing and CDATA edge cases

tests/unit/utils/content-processor.test.ts


12. tests/integration/main-process/handlers.test.ts 🧪 Tests +110/-1

Add XML export integration tests and save dialog filter tests

tests/integration/main-process/handlers.test.ts


13. tests/integration/main-process/xml-export-e2e.test.ts 🧪 Tests +94/-0

New end-to-end test for real XML export with file system

tests/integration/main-process/xml-export-e2e.test.ts


14. tests/mocks/yaml-mock.ts 🧪 Tests +24/-7

Extend YAML mock to support export_format parsing

tests/mocks/yaml-mock.ts


15. tests/unit/components/app.test.tsx 🧪 Tests +62/-0

Add tests for XML export format in app processing and save

tests/unit/components/app.test.tsx


16. tests/unit/components/config-tab.test.tsx 🧪 Tests +23/-0

Add tests for export format selector persistence

tests/unit/components/config-tab.test.tsx


Grey Divider

Qodo Logo

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello @Mehdi-Bl, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the repository content export functionality by introducing a new XML export option alongside the existing Markdown format. The XML output is meticulously crafted to be robust and well-formed, addressing potential issues with special characters and ensuring data integrity. These changes also standardize token count handling and improve the user experience during file saving by aligning default extensions and filters with the chosen export format.

Highlights

  • Export Format Selection: Added support for selecting the export format (Markdown or XML) via configuration, allowing users to choose their preferred output structure.
  • Hardened XML Output: Implemented robust XML generation, including proper attribute escaping, sanitization of invalid XML characters, and safe CDATA wrapping to ensure well-formed and secure XML output.
  • Consistent Token Accounting: Ensured that token accounting is consistent, with per-file XML token attributes and overall summary totals using the same normalized source for accuracy.
  • Improved Save Behavior: Enhanced the save dialog behavior to dynamically suggest the correct default file extension and prioritize filters based on the currently selected export format (e.g., '.xml' for XML exports).

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/main/index.ts
    • Imported new XML export utility functions for normalization, escaping, and CDATA wrapping.
    • Modified the 'repo:process' IPC handler to conditionally generate XML or Markdown output based on the 'exportFormat' option, including XML declarations, root elements, file structure, and file content with token attributes.
    • Updated the 'fs:saveFile' IPC handler to dynamically set save dialog filters and default file extensions ('xml' or 'md') based on the detected export format.
  • src/renderer/components/App.tsx
    • Added 'exportFormat' to the 'ProcessingOptions' type and component state.
    • Updated configuration loading and processing logic to read and apply the 'export_format' setting.
    • Adjusted the 'saveFile' call to use the correct default file extension ('xml' or 'md') based on the configured export format.
  • src/renderer/components/ConfigTab.tsx
    • Introduced 'exportFormat' state and integrated it into the configuration parsing and stringification.
    • Added a new UI selector in the configuration tab for users to choose between 'Markdown' and 'XML' export formats.
  • src/types/ipc.ts
    • Defined a new 'ExportFormat' type ('markdown' | 'xml').
    • Added 'export_format?: ExportFormat;' to 'ConfigObject' and 'ProcessRepositoryOptions['options']'.
  • src/utils/config.default.yaml
    • Added 'export_format: markdown' as a default setting.
  • src/utils/content-processor.ts
    • Imported XML export utility functions.
    • Modified the 'processFile' method to accept 'exportFormat', 'showTokenCount', and 'tokenCount' options.
    • Implemented logic to generate XML output for both text and binary files, including XML attributes for path, binary status, file type, size, and token count, with content wrapped in CDATA.
  • src/utils/export-format.ts
    • New file: Contains helper functions for 'normalizeExportFormat', 'escapeXmlAttribute', 'sanitizeXmlContent' (to remove invalid XML characters), 'wrapXmlCdata' (to handle ']]>' sequences), 'normalizeTokenCount', and 'toXmlNumericAttribute'.
  • tests/fixtures/configs/default.yaml
    • Added 'export_format: markdown'.
  • tests/fixtures/configs/minimal.yaml
    • Added 'export_format: markdown'.
  • tests/integration/main-process/handlers.test.ts
    • Updated the mock for 'ContentProcessor' to simulate XML output when 'exportFormat' is 'xml'.
    • Added a new integration test case to verify the generation of well-formed XML output.
    • Added integration tests for the 'fs:saveFile' handler to ensure correct dynamic filter prioritization based on the default file extension.
  • tests/integration/main-process/xml-export-e2e.test.ts
    • New file: An end-to-end integration test specifically for XML export, verifying the structure, content sanitization, and CDATA handling.
  • tests/mocks/yaml-mock.ts
    • Enhanced the 'yaml.parse' mock to correctly interpret 'export_format', 'include_tree_view', and 'show_token_count' settings from YAML strings for testing purposes.
  • tests/unit/components/app.test.tsx
    • Updated existing tests to assert that 'exportFormat' is correctly passed to 'processRepository'.
    • Added a new unit test to verify that the application correctly uses the XML export format for processing and save paths when configured.
  • tests/unit/components/config-tab.test.tsx
    • Updated the mock configuration to include 'export_format'.
    • Added a unit test to confirm that changes to the export format selection in the UI are correctly persisted in the saved configuration.
  • tests/unit/utils/content-processor.test.ts
    • Added unit tests to verify the 'processFile' method's ability to generate correct XML output for both text and binary files, including proper CDATA wrapping and invalid XML character sanitization.
  • tests/unit/utils/export-format.test.ts
    • New file: Contains comprehensive unit tests for all functions within 'src/utils/export-format.ts', covering normalization, escaping, sanitization, and CDATA wrapping.
Activity
  • Previous review concerns regarding duplicated XML helpers, token consistency, and missing XML edge-case coverage have been addressed in this change set.
  • New integration tests were added for the real XML export path through the main handler and content processor.
  • Utility unit tests were added, and coverage was expanded for XML edge cases and save dialog filter ordering.
  • All linting and jest tests passed successfully during the validation run, ensuring code quality and functionality.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant new feature: the ability to export repository analysis in either Markdown or XML format. While the XML export is well-implemented with proper escaping and sanitization, a critical security vulnerability exists where several IPC handlers in the main process lack robust path validation. This could lead to path traversal and information disclosure, as the current startsWith validation is insufficient and bypassable. It is recommended to use path.relative to ensure all file operations are strictly confined to the intended repository root. Additionally, for performance, consider improving XML content sanitization in src/utils/export-format.ts by using a regular expression.

Comment thread src/main/index.ts Outdated
Comment thread src/main/index.ts Outdated
Comment thread src/main/index.ts Outdated
Comment thread src/utils/export-format.ts Outdated

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • In fs:getDirectoryTree you parse configContent twice (once in the try block building excludePatterns and again immediately after); consider parsing once and reusing the same config instance to avoid divergence between the two branches and to simplify error handling.
  • resolveExportFormatFromConfig in App.tsx re-parses the YAML config just to extract export_format, even though the config is already parsed elsewhere when updating processingOptions; you could derive the export format directly from the stored processingOptions or a shared config parsing path to keep behavior consistent.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `fs:getDirectoryTree` you parse `configContent` twice (once in the `try` block building `excludePatterns` and again immediately after); consider parsing once and reusing the same `config` instance to avoid divergence between the two branches and to simplify error handling.
- `resolveExportFormatFromConfig` in `App.tsx` re-parses the YAML config just to extract `export_format`, even though the config is already parsed elsewhere when updating `processingOptions`; you could derive the export format directly from the stored `processingOptions` or a shared config parsing path to keep behavior consistent.

## Individual Comments

### Comment 1
<location> `src/main/index.ts:547-550` </location>
<code_context>
+          { name: 'All Files', extensions: ['*'] },
+        ];
+
+  const { canceled, filePath } = await dialog.showSaveDialog(mainWindow ?? undefined, {
     defaultPath,
-    filters: [
</code_context>

<issue_to_address>
**suggestion:** Guard the `showSaveDialog` overload instead of passing `undefined` as the BrowserWindow argument

Passing `undefined` into the `BrowserWindow` overload relies on runtime quirks and weakens type safety. Instead, branch on `mainWindow` and call the appropriate overload so you only ever pass a valid window or use the options-only variant.

```suggestion
  const { canceled, filePath } = mainWindow
    ? await dialog.showSaveDialog(mainWindow, {
        defaultPath,
        filters,
      })
    : await dialog.showSaveDialog({
        defaultPath,
        filters,
      });
```
</issue_to_address>

### Comment 2
<location> `src/renderer/components/App.tsx:26-35` </location>
<code_context>
   return new Error(String(error));
 };

+const resolveExportFormatFromConfig = (rawConfigContent: string) => {
+  try {
+    const config = (yaml.parse(rawConfigContent) || {}) as ConfigObject;
</code_context>

<issue_to_address>
**suggestion:** Use a single source of truth for export format instead of re-parsing config on save

`exportFormat` is currently derived in three places: `processingOptions`, `configContent`, and `resolveExportFormatFromConfig` in the save handler, which risks them drifting over time.

Since `processingOptions.exportFormat` is already synced from the config and reflects the UI choice, consider using it directly in the save handler instead of re-parsing YAML, e.g.:

```ts
const exportFormat = processingOptions.exportFormat;
const outputExtension = exportFormat === 'xml' ? 'xml' : 'md';
```

Suggested implementation:

```typescript

```

To fully implement your suggestion, you should also:
1. In the save handler (wherever the export format is currently resolved), replace any usage of `resolveExportFormatFromConfig(configContent)` with:
   ```ts
   const exportFormat = processingOptions.exportFormat;
   const outputExtension = exportFormat === 'xml' ? 'xml' : 'md';
   ```
2. Remove the import(s) for `yaml` and `normalizeExportFormat` from this file **if** they are now only used by the deleted `resolveExportFormatFromConfig` function.
3. Remove any leftover references to `resolveExportFormatFromConfig` to avoid type errors.
These steps ensure `processingOptions.exportFormat` is the single source of truth and prevent config re-parsing on save.
</issue_to_address>

### Comment 3
<location> `tests/unit/components/config-tab.test.tsx:77` </location>
<code_context>
     expect(screen.getByLabelText('Apply .gitignore rules')).toBeChecked();
     expect(screen.getByLabelText('Scan content for secrets')).toBeChecked();
     expect(screen.getByLabelText('Exclude suspicious files')).toBeChecked();
+    expect(screen.getByLabelText('Export format')).toHaveValue('markdown');

     // Check textareas
</code_context>

<issue_to_address>
**suggestion (testing):** Add a test for initializing the export format selector when the config specifies `export_format: xml`.

Current tests only cover the default (`markdown`) and persistence of changes. Please also add a test where `configContent` includes `export_format: xml`, render `ConfigTab`, and assert that the `<select>` value is `xml`. This will exercise `updateConfigStates`/`normalizeExportFormat` and ensure existing XML configs hydrate correctly.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/main/index.ts Outdated
Comment thread src/renderer/components/App.tsx Outdated
Comment thread tests/unit/components/config-tab.test.tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4205dfb1d9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/utils/content-processor.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
src/main/index.ts (1)

378-396: ⚠️ Potential issue | 🟡 Minor

Tree rendering bug: isLast and isLastItem are swapped, producing wrong connectors.

isLast (the parent's position) is used on lines 386 and 390 to choose the current node's connector (└── vs ├──) and prefix continuation ( vs ). It should be isLastItem (the current node's position among siblings).

For example, a tree {A: {C: null}, B: null} currently renders:

└── A        ← wrong (should be ├── A)
    ├── C    ← wrong (should be └── C)
└── B
Proposed fix
     entries.forEach(([key, value], index) => {
       const isLastItem = index === entries.length - 1;
 
       // Print current level
-      result += `${prefix}${isLast ? '└── ' : '├── '}${key}\n`;
+      result += `${prefix}${isLastItem ? '└── ' : '├── '}${key}\n`;
 
       // Print children
       if (value !== null) {
-        const newPrefix = `${prefix}${isLast ? '    ' : '│   '}`;
+        const newPrefix = `${prefix}${isLastItem ? '    ' : '│   '}`;
         result += printTree(value, newPrefix, isLastItem);
       }
     });
🤖 Fix all issues with AI agents
In `@src/main/index.ts`:
- Around line 273-275: The check for config.use_gitignore in the repo:analyze
path is inconsistent with getDirectoryTree; change the conditional that sets
gitignorePatterns (currently "if (config.use_gitignore === true)") to mirror
getDirectoryTree's behavior ("if (config.use_gitignore !== false)") so undefined
defaults to enabled; update the conditional around gitignorePatterns =
gitignoreParser.parseGitignore(rootPath) (and any related logic in the
repo:analyze flow) to use !== false to keep behavior consistent with
getDirectoryTree.
- Around line 134-193: The code parses configContent twice (yaml.parse) and the
second parse at the bottom lacks error handling; move the parsing of
configContent into the existing try block so a single parsed ConfigObject (e.g.,
variable config) is created once and reused, remove the duplicate parse after
the try-catch, and ensure that on parse failure you still set excludePatterns =
['**/.git/**'] and provide a fallback config (e.g., { exclude_patterns: [] })
before creating localShouldExclude which should call shouldExclude(itemPath,
dirPath, excludePatterns, config).
🧹 Nitpick comments (5)
src/utils/export-format.ts (1)

22-33: Consider using an array join for sanitizeXmlContent on large inputs.

String concatenation in a loop (sanitized += char) creates intermediate strings on each iteration. For very large file contents this can be a performance concern. An array-based approach avoids quadratic copying behavior in some engines.

♻️ Optional refactor
 export const sanitizeXmlContent = (value: string): string => {
-  let sanitized = '';
-
+  const parts: string[] = [];
   for (const char of value) {
     const codePoint = char.codePointAt(0);
     if (codePoint !== undefined && isValidXmlCodePoint(codePoint)) {
-      sanitized += char;
+      parts.push(char);
     }
   }
-
-  return sanitized;
+  return parts.join('');
 };
tests/integration/main-process/xml-export-e2e.test.ts (1)

13-48: Consider verifying jest.isolateModules re-registration of IPC handlers.

jest.resetModules() is called at the top of beforeEach, but mockIpcHandlers is a module-level const object whose keys persist across tests. If additional tests are added to this suite, handlers from a previous test could leak. Consider clearing mockIpcHandlers at the start of beforeEach:

Object.keys(mockIpcHandlers).forEach(k => delete mockIpcHandlers[k]);

This is a minor robustness concern for future test additions since there's currently only one test case.

src/utils/content-processor.ts (1)

80-93: Avoid computing token count when showTokenCount is false.

When showTokenCount is falsy, resolvedTokenCount is still computed (Line 81-84). If tokenCount isn't provided in options, this triggers this.tokenCounter.countTokens(content) which runs tiktoken encoding — potentially expensive for large files — only to discard the result.

♻️ Suggested optimization
      if (exportFormat === 'xml') {
-       const resolvedTokenCount =
-         options.tokenCount !== undefined
-           ? normalizeTokenCount(options.tokenCount)
-           : this.tokenCounter.countTokens(content);
-       const tokenAttribute = options.showTokenCount
-         ? ` tokens="${escapeXmlAttribute(String(resolvedTokenCount))}"`
-         : '';
+       let tokenAttribute = '';
+       if (options.showTokenCount) {
+         const resolvedTokenCount =
+           options.tokenCount !== undefined
+             ? normalizeTokenCount(options.tokenCount)
+             : this.tokenCounter.countTokens(content);
+         tokenAttribute = ` tokens="${escapeXmlAttribute(String(resolvedTokenCount))}"`;
+       }
        return (
          `<file path="${escapeXmlAttribute(relativePath)}"${tokenAttribute} binary="false">\n` +
src/renderer/components/App.tsx (1)

270-283: Config parsing for processing options is duplicated across handlers.

The pattern of parsing YAML config → extracting showTokenCount, includeTreeView, exportFormat appears in handleAnalyze (Lines 276-283), handleRefreshProcessed (Lines 344-354), and handleTabChange (Lines 137-148). Consider extracting a shared helper like resolveProcessingOptionsFromConfig(rawConfig: string): ProcessingOptions to reduce duplication and ensure consistency.

♻️ Example consolidation
const resolveProcessingOptionsFromConfig = (rawConfigContent: string): ProcessingOptions => {
  try {
    const config = (yaml.parse(rawConfigContent) || {}) as ConfigObject;
    return {
      showTokenCount: config.show_token_count !== false,
      includeTreeView: config.include_tree_view === true,
      exportFormat: normalizeExportFormat(config.export_format),
    };
  } catch {
    return { showTokenCount: true, includeTreeView: false, exportFormat: 'markdown' };
  }
};

This replaces resolveExportFormatFromConfig and the inline parsing in each handler.

src/main/index.ts (1)

267-270: New TokenCounter instances created per IPC call despite a global singleton at line 33.

Both repo:analyze (line 269) and repo:process (line 412) create a fresh TokenCounter, each re-initializing the tiktoken encoder. The module-level singleton tokenCounter (line 33) already exists and is used by tokens:countFiles. Consider reusing it here unless there's a specific reason for isolation.

Also applies to: 411-413

Comment thread src/main/index.ts
Comment thread src/main/index.ts Outdated
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Feb 9, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (3) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Unsafe startsWith root check 📘 Rule violation ⛨ Security
Description
• The IPC handlers validate that a provided path is “inside” rootPath using startsWith, which is
  bypassable (e.g., /root-evil/... or absolute paths that share the same prefix).
• This can allow processing/reading files outside the intended repository root, violating the
  requirement to securely validate external inputs.
• The current normalizePath() only normalizes slashes and does not enforce a path-boundary check,
  so the prefix issue remains.
Code

src/main/index.ts[R472-480]

+          // Validate the full path is within the root path
+          const normalizedFullPath = normalizePath(fullPath);
+          const normalizedRootPath = normalizePath(rootPath);
-        if (!normalizedFullPath.startsWith(normalizedRootPath)) {
-          console.warn(`Skipping file outside root directory: ${filePath}`);
-          skippedFiles++;
-          continue;
-        }
+          if (!normalizedFullPath.startsWith(normalizedRootPath)) {
+            console.warn(`Skipping file outside root directory: ${filePath}`);
+            skippedFiles++;
+            continue;
+          }
Evidence
PR Compliance ID 6 requires validation/sanitization of external inputs (IPC-provided paths). The
code uses prefix checks (startsWith) for both selectedFiles and per-file processing, which is
not a safe containment check; normalizePath does not mitigate this because it only replaces
backslashes.

Rule 6: Generic: Security-First Input Validation and Data Handling
src/main/index.ts[288-293]
src/main/index.ts[472-480]
src/utils/filter-utils.ts[8-11]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`repo:analyze` and `repo:process` attempt to ensure file paths stay within `rootPath` using string prefix checks (`startsWith`). This is bypassable and can allow reading/processing files outside the intended root.
## Issue Context
These handlers receive path inputs via IPC (renderer-controlled), so they must treat paths as external inputs and validate them safely.
## Fix Focus Areas
- src/main/index.ts[288-323]
- src/main/index.ts[466-480]
- src/utils/filter-utils.ts[8-12]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Invalid XML attributes 🐞 Bug ✓ Correctness
Description
• XML attribute escaping only replaces markup-sensitive characters and does not remove invalid XML
  1.0 code points.
• File paths (and other attribute values) can contain control characters on some platforms; this
  would produce non-well-formed XML even though file *content* is sanitized.
• XML export should sanitize attribute values similarly to CDATA content sanitization.
Code

src/utils/export-format.ts[R14-20]

+export const escapeXmlAttribute = (value: string): string =>
+  value
+    .replace(/&/g, '&amp;')
+    .replace(/"/g, '&quot;')
+    .replace(/'/g, '&apos;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;');
Evidence
The codebase introduces sanitizeXmlContent() to filter invalid XML code points, but
escapeXmlAttribute() does not use it. Attributes like path are escaped but not sanitized, so
invalid code points can leak into the document structure.

src/utils/export-format.ts[14-20]
src/utils/export-format.ts[22-33]
src/utils/content-processor.ts[49-55]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`escapeXmlAttribute` escapes markup characters but does not filter invalid XML code points. XML export uses it for attributes (e.g., `path`), which can produce invalid XML.
### Issue Context
You already implemented `sanitizeXmlContent()` for CDATA content; attributes should be treated similarly.
### Fix Focus Areas
- src/utils/export-format.ts[14-33]
- src/utils/content-processor.ts[49-55]
- src/utils/content-processor.ts[80-92]
### Suggested approach
- Update `escapeXmlAttribute(value)` to first run `sanitizeXmlContent(value)` (or an equivalent sanitizer) and then perform entity escaping.
- Add/adjust unit tests to cover attribute values containing control chars (e.g. `\u0001`) to ensure output XML remains well-formed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. File error log lacks context 📘 Rule violation ⛯ Reliability
Description
• When a file fails to process, the warning log only prints getErrorMessage(error) and omits which
  filePath failed.
• This reduces actionable debugging context and makes it harder to diagnose edge-case failures
  during export/processing.
Code

src/main/index.ts[R498-500]

+        } catch (error) {
+          console.warn(`Failed to process file: ${getErrorMessage(error)}`);
skippedFiles++;
Evidence
PR Compliance ID 3 requires meaningful context in error handling. The per-file catch block logs a
generic message without including the associated filePath, so it’s not actionable when multiple
files are processed.

Rule 3: Generic: Robust Error Handling and Edge Case Management
src/main/index.ts[466-467]
src/main/index.ts[498-501]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Per-file processing failures are logged without identifying which file failed.
## Issue Context
`repo:process` iterates over many files; without the file path in the log, failures are not actionable.
## Fix Focus Areas
- src/main/index.ts[458-501]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Test catalog missing new tests 📘 Rule violation ⛯ Reliability
Description
• New test files were added for XML export (xml-export-e2e.test.ts) and export-format utilities,
  but tests/catalog.md does not list them.
• This causes the documented test targets/use cases to drift from actual coverage, violating the
  catalog alignment requirement.
Code

tests/integration/main-process/xml-export-e2e.test.ts[R1-12]

+const os = require('os');
+const path = require('path');
+
+jest.unmock('fs');
+jest.unmock('../../../src/utils/content-processor');
+
+const fs = require('fs');
+
+describe('XML export end-to-end', () => {
+  const mockIpcHandlers = {};
+  let tempRoot = '';
+
Evidence
PR Compliance ID 9 requires tests/catalog.md to be updated when test targets/use cases change. The
PR adds new test suites, but the catalog’s Unit/Integration test tables do not include these new
files.

AGENTS.md
tests/integration/main-process/xml-export-e2e.test.ts[9-10]
tests/unit/utils/export-format.test.ts[10-16]
tests/catalog.md[29-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`tests/catalog.md` is missing entries for newly added/expanded test coverage in this PR.
## Issue Context
The catalog is the authoritative map for test targets and use cases; it should be updated when new test suites are introduced.
## Fix Focus Areas
- tests/catalog.md[11-59]
- tests/integration/main-process/xml-export-e2e.test.ts[1-94]
- tests/unit/utils/export-format.test.ts[1-42]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


✅ 5. Unguarded YAML re-parse 🐞 Bug ⛯ Reliability
Description
fs:getDirectoryTree catches YAML parse errors once, but then parses the same configContent
  again outside the try/catch.
• If configContent is invalid YAML, the handler can still throw and reject the IPC call, breaking
  the Source tab tree load/refresh.
• This is avoidable by parsing once and reusing the result (or safely defaulting).
Code

src/main/index.ts[R183-193]

+    // Import the fnmatch module
-  // Use the shared shouldExclude function from filter-utils
-  const localShouldExclude = (itemPath: string) => {
-    return shouldExclude(itemPath, dirPath, excludePatterns, config);
-  };
+    // Get the config for filtering
+    const config = (configContent
+      ? (yaml.parse(configContent) as ConfigObject)
+      : ({ exclude_patterns: [] } as ConfigObject)) || { exclude_patterns: [] };
-  const walkDirectory = (dir: string): DirectoryTreeItem[] => {
-    const items = fs.readdirSync(dir);
-    const result: DirectoryTreeItem[] = [];
+    // Use the shared shouldExclude function from filter-utils
+    const localShouldExclude = (itemPath: string) => {
+      return shouldExclude(itemPath, dirPath, excludePatterns, config);
+    };
Evidence
After the guarded parse, the function immediately performs another unguarded
yaml.parse(configContent) to create config for filtering, so invalid YAML still fails the
handler despite the earlier catch.

src/main/index.ts[133-189]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`fs:getDirectoryTree` parses `configContent` in a try/catch to build `excludePatterns`, but then immediately parses it again outside the try/catch to build `config` for filtering. Invalid YAML can still throw and reject the IPC handler.
### Issue Context
This handler is on the hot path for loading and refreshing the Source tab tree.
### Fix Focus Areas
- src/main/index.ts[133-193]
### Suggested approach
- Introduce a single `let parsedConfig: ConfigObject = { exclude_patterns: [] } as ConfigObject;`
- In the try block, set `parsedConfig = ...yaml.parse...`.
- In the catch block, keep `parsedConfig` as the fallback and set `excludePatterns` fallback.
- Use `parsedConfig` in `shouldExclude(itemPath, dirPath, excludePatterns, parsedConfig)`.
- Remove the second `yaml.parse` entirely.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
✅ 6. Save format mismatch 🐞 Bug ✓ Correctness
Description
• Saving output chooses the file extension based on the *current* config, not on the format that
  produced processedResult.content.
• If the user changes export_format after processing (or the config differs from what was used for
  the current processed view), the app can suggest output.xml while saving markdown (or vice versa).
• This is a correctness/UX issue and can break downstream tooling relying on extension/content
  consistency.
Code

src/renderer/components/App.tsx[R390-395]

+      const exportFormat = resolveExportFormatFromConfig(configContent);
+      const outputExtension = exportFormat === 'xml' ? 'xml' : 'md';
await window.electronAPI?.saveFile?.({
content: processedResult.content,
-        defaultPath: `${rootPath}/output.md`,
+        defaultPath: `${rootPath}/output.${outputExtension}`,
});
Evidence
handleSaveOutput derives extension from configContent at save time, while the processed content
format is not tracked in ProcessRepositoryResult, so the app cannot reliably know the content’s
actual format.

src/renderer/components/App.tsx[383-395]
src/types/ipc.ts[59-65]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Save action derives the output extension from current config, which may differ from the export format used to generate `processedResult.content`.
### Issue Context
`ProcessRepositoryResult` does not include `exportFormat`, so the renderer cannot know what format the content is without guessing from config.
### Fix Focus Areas
- src/renderer/components/App.tsx[383-395]
- src/types/ipc.ts[48-65]
- src/main/index.ts[415-521]
### Suggested approach
Option A (most robust):
- Add `exportFormat: ExportFormat` to `ProcessRepositoryResult`.
- In `repo:process` handler, set `exportFormat: processingOptions.exportFormat` in the return.
- In `App`, use `processedResult.exportFormat` to decide `outputExtension`.
Option B (renderer-only):
- Store `exportFormatUsedForProcessedResult` in state at the time you call `processRepository`, and use that state for saving (not `configContent`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


✅ 7. Slow XML sanitization 🐞 Bug ➹ Performance
Description
sanitizeXmlContent uses sanitized += char inside a loop, which can degrade to quadratic
  behavior due to repeated string reallocations.
• XML export sanitizes full file contents and potentially large tree views, so this can become a
  noticeable performance/memory issue for large repositories.
• Prefer building into an array and join() (or another linear-time strategy).
Code

src/utils/export-format.ts[R22-31]

+export const sanitizeXmlContent = (value: string): string => {
+  let sanitized = '';
+
+  for (const char of value) {
+    const codePoint = char.codePointAt(0);
+    if (codePoint !== undefined && isValidXmlCodePoint(codePoint)) {
+      sanitized += char;
+    }
+  }
+
Evidence
wrapXmlCdata always calls sanitizeXmlContent, and sanitizeXmlContent concatenates strings in a
loop. wrapXmlCdata is used for the XML file structure and file contents, so large exports amplify
this cost.

src/utils/export-format.ts[22-31]
src/utils/export-format.ts[35-36]
src/main/index.ts[433-438]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`sanitizeXmlContent()` builds the sanitized output using `+=` in a loop, which can cause poor performance for large inputs.
### Issue Context
This code runs on full file contents and tree views during XML export.
### Fix Focus Areas
- src/utils/export-format.ts[22-36]
### Suggested approach
- Replace `let sanitized = &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;#x27;&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;#x27;` / `sanitized += char` with:
- `const out: string[] = [];` and `out.push(char)` for valid chars
- `return out.join(&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;#x27;&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;#x27;);`
- Keep current behavior for code point validation and `]]&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt;` splitting.
- Consider adding a micro-benchmark-ish unit test (optional) or at least ensure existing tests still pass.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment thread src/main/index.ts Outdated
Comment thread src/utils/export-format.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/main/index.ts`:
- Around line 284-287: The code parses gitignorePatterns when
config.use_gitignore !== false but then constructs FileAnalyzer with
useGitignore: config.use_gitignore === true, causing parsed patterns to be
ignored when use_gitignore is undefined; update the FileAnalyzer construction to
pass useGitignore: config.use_gitignore !== false (or otherwise make both checks
the same) so that when use_gitignore is not set it defaults to enabled and the
parsed gitignorePatterns are applied to FileAnalyzer.

In `@tests/integration/main-process/handlers.test.ts`:
- Around line 40-56: The mocked 'path' in the test uses POSIX for relative but
leaves resolve and isAbsolute platform-native which breaks Windows CI; update
the jest.mock('path', ...) block to also mock resolve and isAbsolute
consistently with POSIX (use realPath.posix.resolve for resolve and
realPath.posix.isAbsolute for isAbsolute) so that functions like
isPathWithinRoot, and tests referencing path.resolve/path.isAbsolute behave
correctly across platforms.
🧹 Nitpick comments (1)
src/main/index.ts (1)

111-111: FilterPatternBundle attaches ad-hoc properties to an array — consider a proper interface.

Mutating a string[] with .includePatterns and .includeExtensions is fragile and hides the data shape. A dedicated type would be clearer and safer.

Example refactor
-type FilterPatternBundle = string[] & { includePatterns?: string[]; includeExtensions?: string[] };
+interface FilterPatternBundle {
+  excludePatterns: string[];
+  includePatterns?: string[];
+  includeExtensions?: string[];
+}

Then update usages to reference bundle.excludePatterns instead of spreading the array directly.

Comment thread src/main/index.ts
Comment thread tests/integration/main-process/handlers.test.ts
@Mehdi-Bl

Mehdi-Bl commented Feb 9, 2026

Copy link
Copy Markdown
Contributor Author

Addressed review feedback in commits 6474187 and 47ab23b.

Implemented fixes:

  • Hardened path validation in main IPC handlers using root boundary checks with resolve/relative logic.
  • Added root-scoped validation for tokens:countFiles and updated IPC payload shape.
  • Removed duplicate YAML parsing in fs:getDirectoryTree and aligned gitignore default behavior.
  • Hardened save dialog overload handling and missing filePath handling.
  • Simplified XML content sanitization with a single regex and sanitized XML attributes.
  • Removed duplicate export-format parsing in App and used processing options as single source of truth.
  • Added/updated integration and unit tests covering traversal protection, XML attribute sanitization, export-format hydration, and Windows-safe path mocking.

Validation run locally:

  • npm run lint
  • npm test -- --runInBand
  • npm run build:ts
  • pre-commit run --files on changed files

All PR checks are now passing, including QA matrix (ubuntu, macos, windows), CodeQL, sonarcloud, zizmor, gitleaks, dependency-review, sbom, and bot reviews.

@Mehdi-Bl

Mehdi-Bl commented Feb 9, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up commit pushed: 82e47c1

Final fixes included:

  • Aligned repo:analyze gitignore default with tree behavior (use_gitignore !== false).
  • Hardened path resolution by resolving selected/token-count file paths against rootPath before boundary checks.
  • Added integration coverage for relative in-root paths and traversal rejection in both repo:analyze and tokens:countFiles.

Validation rerun locally:

  • npm run lint
  • npm test -- --runInBand
  • npm run build:ts
  • pre-commit run --files src/main/index.ts tests/integration/main-process/handlers.test.ts

All PR checks are green.

@Mehdi-Bl

Mehdi-Bl commented Feb 9, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up gap fixes pushed in aa55686.

What was fixed:

  • Prevented save-format drift by returning the actual format used for generation in ProcessRepositoryResult.exportFormat and using that in save path resolution.
  • Updated XML integration assertions to verify exportFormat in results.
  • Added regression unit test for: process as markdown, then change config to xml, save still uses .md.
  • Updated tests/catalog.md to include XML export E2E and export-format unit coverage.

Validation rerun locally:

  • npm run lint
  • npm test -- --runInBand (201 passed)
  • npm run build:ts
  • pre-commit run --files ... on changed files

@coderabbitai review
@sourcery-ai review

Requesting re-review.

@coderabbitai

coderabbitai Bot commented Feb 9, 2026

Copy link
Copy Markdown

@Mehdi-Bl, thank you for the thorough follow-up! I can see you've addressed the save-format drift issue by ensuring ProcessRepositoryResult.exportFormat returns the actual format used, which is then consistently applied in save path resolution. The added regression test for the markdown→xml config change scenario is a great catch for preventing future drift.

I'll kick off a fresh review of the changes now.


🧠 Learnings used
Learnt from: CR
Repo: codingworkflow/ai-code-fusion PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-08T21:09:09.714Z
Learning: Before proposing merge-ready changes, run: `npm run lint`, `npm test -- --runInBand`, and `npm run qa:screenshot` (if UI behavior/layout is changed)
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
src/main/index.ts (1)

86-91: ⚠️ Potential issue | 🔴 Critical

Use isPathWithinRoot() to validate the assets protocol path to prevent directory traversal.

The assets:// protocol handler lacks path boundary validation. Although path.normalize(path.join(PUBLIC_ASSETS_DIR, url)) resolves symlinks, it does not prevent traversal outside PUBLIC_ASSETS_DIR—a crafted URL like assets://../../etc/passwd would resolve to /etc/passwd. The codebase already implements isPathWithinRoot() for this exact purpose (lines 254–264) and uses it consistently for user-provided paths elsewhere (lines 298, 480, 619). Apply the same validation here:

const url = request.url.replace('assets://', '');
const assetPath = path.resolve(PUBLIC_ASSETS_DIR, url);
if (!isPathWithinRoot(PUBLIC_ASSETS_DIR, assetPath)) {
  callback({ error: new Error('Access denied') });
  return;
}
callback({ path: assetPath });
🧹 Nitpick comments (2)
src/main/index.ts (2)

419-421: tokenCounter shadows the module-level singleton unnecessarily.

Line 420 creates a new TokenCounter instance that shadows the singleton declared at line 34. The tokens:countFiles handler (line 650) reuses the singleton. Consider reusing it here too for consistency and to avoid unnecessary allocations.

Suggested fix
     try {
-      const tokenCounter = new TokenCounter();
-      const contentProcessor = new ContentProcessor(tokenCounter);
+      const contentProcessor = new ContentProcessor(tokenCounter);

434-460: XML document assembly via string concatenation works but is verbose.

The XML output is built through repeated += on processedContent. This is functional and correct, but as the XML structure grows, consider extracting this into a dedicated builder or template function to improve readability and maintainability.

@Mehdi-Bl

Mehdi-Bl commented Feb 9, 2026

Copy link
Copy Markdown
Contributor Author

Fresh follow-up commit pushed: 1431629.

Addressed latest gaps from external review:

  • Hardened renderer-side root boundary checks to prevent prefix-collision acceptance before IPC calls.
  • Aligned showTokenCount fallback handling in repo:process to avoid default mismatch paths.
  • Added regression tests for:
    • prefix-collision file selection rejection in App
    • markdown file-contents heading behavior when tree view is disabled
    • XML token-attribute omission unless showTokenCount is explicitly true

Validation rerun locally:

  • npm run lint
  • npm test -- --runInBand (205 passed)
  • npm run build:ts
  • pre-commit run --files ...

@gemini-code-assist please re-review the latest changes for any remaining critical/high gaps in security, reliability, or behavior regressions.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
src/renderer/components/App.tsx (1)

342-363: ⚠️ Potential issue | 🟡 Minor

handleRefreshProcessed skips the file-validation step that handleAnalyze performs.

handleAnalyze (line 228–237) filters selectedFiles through isPathWithinRootBoundary before sending them to analyzeRepository. handleRefreshProcessed (line 362) passes selectedFiles directly without re-validation.

If the user changes the root directory between analyze and refresh, stale paths could be sent. The main process also validates, so this isn't exploitable, but it means the renderer would miss the "No valid files" early-exit UX (alert + throw) that handleAnalyze provides.

Suggested alignment
+      // Re-validate selected files against current root
+      const validFiles = selectedFiles.filter((file) => isPathWithinRootBoundary(file));
+      if (validFiles.length === 0) {
+        alert('No valid files selected. Please go to the Source tab and re-select files.');
+        return null;
+      }
+
       // Run a fresh analysis to re-read all files from disk
       const currentReanalysisResult = await window.electronAPI.analyzeRepository({
         rootPath,
         configContent,
-        selectedFiles: selectedFiles,
+        selectedFiles: validFiles,
       });
🧹 Nitpick comments (3)
src/main/index.ts (2)

273-275: Unguarded yaml.parse can throw on malformed configContent.

If the caller sends malformed YAML, yaml.parse(configContent) throws before the || {} fallback. The outer try/catch at line 273 will propagate this as a rejected IPC promise (line 346: throw error). This is acceptable but differs from fs:getDirectoryTree which silently falls back. Consider whether a hard failure is intentional here or if a fallback config would be more resilient.


419-421: Redundant TokenCounter allocation shadows the module-level singleton.

Line 420 creates a new TokenCounter instance each time repo:process is called, while there's already a singleton at line 34. The repo:process handler uses this local instance only via ContentProcessor, and ContentProcessor.processFile may fall back to it for non-XML paths (or when tokenCount isn't passed). Since tokenCount is always supplied here (line 490), the local counter is effectively unused for XML, but still constructed.

Consider reusing the module-level tokenCounter to avoid repeated encoder initialization.

Suggested diff
-      const tokenCounter = new TokenCounter();
-      const contentProcessor = new ContentProcessor(tokenCounter);
+      const contentProcessor = new ContentProcessor(tokenCounter); // reuse module-level singleton
src/renderer/components/App.tsx (1)

303-325: Custom path normalization re-implements path.resolve — verify edge-case parity.

This is a hand-rolled path resolver for the renderer (which lacks Node's path module). A few edge cases to be aware of:

  1. UNC paths (\\server\share\...): the leading \\ is normalized to //, but the drive-letter logic won't match, so the path becomes //server/share/.... The root path likely won't have this form, so boundary checks will correctly reject — but worth verifying if Windows network paths are a supported use case.
  2. Unresolvable .. segments are silently dropped (line 314–318): e.g., ../../etc/passwd becomes etc/passwd. This is safe for boundary checking since it won't match the root prefix, but the normalized path doesn't represent the actual filesystem path.

Since the main process enforces its own isPathWithinRoot (using Node's path.resolve/path.relative), this is defense-in-depth and the renderer check failing open wouldn't be exploitable. The implementation is reasonable for its purpose.

Consider adding a brief comment explaining why a custom normalizer is needed (no Node path in renderer) and documenting the intentional ..-dropping behavior, so future maintainers don't accidentally rely on this for canonical path resolution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new XML export format alongside the existing Markdown support. It includes comprehensive hardening for XML generation, such as character sanitization, attribute escaping, and safe CDATA wrapping. The changes also align token accounting across the application and improve the save dialog UX by adapting file filters to the selected export format. Additionally, it improves path validation in several IPC handlers by using a more robust isPathWithinRoot check. However, a significant security flaw remains: the IPC handlers trust the rootPath provided by the renderer without verifying it against a trusted source, which allows a compromised renderer to bypass intended directory restrictions and read arbitrary files or list arbitrary directories on the system.

Comment thread src/main/index.ts
@@ -120,136 +127,141 @@ ipcMain.handle('dialog:selectDirectory', async () => {
ipcMain.handle(
'fs:getDirectoryTree',
async (_event, dirPath: string, configContent?: string | null) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-high high

The fs:getDirectoryTree IPC handler accepts an unvalidated dirPath from the renderer and uses it to list directory contents. A compromised renderer can provide an arbitrary path (e.g., /etc or C:\Users) to explore the host file system, leading to information disclosure.

Remediation: The main process should maintain a record of the 'authorized' root path (set during the dialog:selectDirectory call). Verify that the dirPath provided by the renderer is within this authorized path using the isPathWithinRoot helper.

Comment thread src/main/index.ts
@@ -258,79 +270,81 @@ ipcMain.handle(
_event,
{ rootPath, configContent, selectedFiles }: AnalyzeRepositoryOptions

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-high high

The repo:analyze IPC handler trusts the rootPath provided by the renderer. While it uses isPathWithinRoot to ensure selectedFiles are within the provided rootPath, it does not verify that the rootPath itself is authorized. An attacker who has compromised the renderer can set rootPath to / and selectedFiles to sensitive files (e.g., ['etc/passwd']) to read their contents.

Remediation: Store the authorized root path in the main process after a successful directory selection. Use the stored authorized path instead of the one provided by the renderer, or verify the renderer's input against the stored path.

Comment thread src/main/index.ts
'repo:process',
async (
_event,
{ rootPath, filesInfo, treeView, options = {} }: ProcessRepositoryOptions

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-high high

The repo:process IPC handler trusts the rootPath provided by the renderer. This allows a compromised renderer to bypass directory restrictions by providing an arbitrary root path, enabling unauthorized reading of files across the system.

Remediation: Validate the rootPath against a trusted, main-process-side record of the directory the user explicitly authorized via the native selection dialog.

Comment thread src/main/index.ts
results[filePath] = 0;
continue;
}
async (_event, options: CountFilesTokensOptions): Promise<CountFilesTokensResult> => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-high high

The tokens:countFiles IPC handler trusts the rootPath provided by the renderer. This allows a compromised renderer to read and count tokens for any file on the system by providing an arbitrary root path.

Remediation: Ensure the rootPath used for file operations is validated against the path authorized by the user in the main process.

@Mehdi-Bl

Mehdi-Bl commented Feb 9, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up commit pushed: 3fe130b.

Addressed additional external-review findings:

  • Added path-boundary validation in assets:// protocol handler (registerFileProtocol) to block traversal (assets://../../...).
  • Added defensive handling for missing/invalid defaultPath in fs:saveFile.
  • Preserved renderer-side boundary hardening and added tests for protocol traversal + XML token visibility behavior.

New/updated test coverage:

  • tests/integration/main-process/handlers.test.ts
    • assets protocol traversal rejection
    • assets protocol valid path resolution
    • XML output omits tokens when showTokenCount: false
    • fs:saveFile handles missing defaultPath

Validation rerun locally:

  • npm run lint
  • npm test -- --runInBand (211 passed)
  • npm run build:ts
  • pre-commit run --files ...

@gemini-code-assist please review latest commit 3fe130b and flag any remaining critical/high issues.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant new feature for exporting repository content in either Markdown or XML format, complete with a UI selector in the configuration. It also hardens path validation across multiple IPC handlers to prevent path traversal vulnerabilities. However, a critical security concern remains: the IPC boundary between the renderer and main process is vulnerable to arbitrary file system access because the main process trusts the renderer to provide the base directory for operations. This should be addressed by implementing a main-process-side allow-list for authorized paths. Additionally, minor improvements related to performance and code style have been identified.

Comment thread src/main/index.ts
Comment on lines 417 to 421
ipcMain.handle(
'repo:process',
async (
_event,
{ rootPath, filesInfo, treeView, options = {} }: ProcessRepositoryOptions

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-high high

The repo:process IPC handler trusts the rootPath provided by the renderer, allowing for arbitrary file read if the renderer is compromised. Similar to repo:analyze, the rootPath should be validated against a main-process-side allow-list of authorized directories.

Comment thread src/main/index.ts
Comment on lines 612 to +614
ipcMain.handle(
'tokens:countFiles',
async (_event, filePaths: string[]): Promise<CountFilesTokensResult> => {
try {
const results: Record<string, number> = {};
const stats: Record<string, { size: number; mtime: number }> = {};

// Process each file
for (const filePath of filePaths) {
try {
// Check if file exists
if (!fs.existsSync(filePath)) {
console.warn(`File not found for token counting: ${filePath}`);
results[filePath] = 0;
continue;
}
async (_event, options: CountFilesTokensOptions): Promise<CountFilesTokensResult> => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-high high

The tokens:countFiles IPC handler trusts the rootPath provided by the renderer, allowing for arbitrary file read if the renderer is compromised. The rootPath should be validated against a main-process-side allow-list of authorized directories.

Comment thread src/main/index.ts
Comment on lines 131 to 133
ipcMain.handle(
'fs:getDirectoryTree',
async (_event, dirPath: string, configContent?: string | null) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-medium medium

The fs:getDirectoryTree IPC handler trusts the dirPath provided by the renderer without any validation. This allows a compromised renderer process to list any directory on the system. While Electron's context isolation is enabled, this still represents a significant information disclosure risk. Consider validating that dirPath is a path previously authorized by the user (e.g., via dialog:selectDirectory).

Comment on lines +303 to +342
const normalizePathForBoundaryCheck = (inputPath: string): string => {
const normalizedSlashes = inputPath.replace(/\\/g, '/');
const driveMatch = normalizedSlashes.match(/^[A-Za-z]:/);
const drivePrefix = driveMatch ? driveMatch[0].toLowerCase() : '';
const pathWithoutDrive = drivePrefix ? normalizedSlashes.slice(2) : normalizedSlashes;
const hasLeadingSlash = pathWithoutDrive.startsWith('/');

const segments = pathWithoutDrive.split('/').filter((segment) => segment && segment !== '.');
const resolvedSegments: string[] = [];

for (const segment of segments) {
if (segment === '..') {
if (resolvedSegments.length > 0 && resolvedSegments[resolvedSegments.length - 1] !== '..') {
resolvedSegments.pop();
} else if (!hasLeadingSlash) {
// Preserve relative parent traversals so boundary checks can reject them.
resolvedSegments.push('..');
}
continue;
}

resolvedSegments.push(segment);
}

return `${drivePrefix}${hasLeadingSlash ? '/' : ''}${resolvedSegments.join('/')}`;
};

const isPathWithinRootBoundary = (candidatePath: string): boolean => {
if (!candidatePath || !rootPath) {
return false;
}

const normalizedRootPath = normalizePathForBoundaryCheck(rootPath);
const normalizedCandidatePath = normalizePathForBoundaryCheck(candidatePath);

return (
normalizedCandidatePath === normalizedRootPath ||
normalizedCandidatePath.startsWith(`${normalizedRootPath}/`)
);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The helper functions normalizePathForBoundaryCheck and isPathWithinRootBoundary are defined inside the App component, which causes them to be recreated on every render. For better performance and code organization, they should be moved to the module scope outside the component definition.

To make isPathWithinRootBoundary a pure function, you can modify its signature to accept rootPath as a parameter, like so: (candidatePath: string, rootPath: string). Consequently, all its call sites (e.g., lines 229, 438, 460, etc.) will need to be updated to pass the rootPath state.

Comment thread src/main/index.ts
Comment on lines +423 to +534
try {
const tokenCounter = new TokenCounter();
const contentProcessor = new ContentProcessor(tokenCounter);

// Ensure options is an object with default values if missing
const processingOptions = {
showTokenCount: options.showTokenCount !== false, // Default to true if not explicitly false
includeTreeView: options.includeTreeView === true,
exportFormat: normalizeExportFormat(options.exportFormat),
};

console.log('Processing with options:', processingOptions);

let processedContent = '';

if (processingOptions.exportFormat === 'xml') {
processedContent += '<?xml version="1.0" encoding="UTF-8"?>\n';
processedContent += '<repositoryContent>\n';
} else {
processedContent += '# Repository Content\n\n';
}

// If treeView was provided, use it, otherwise generate a more complete one
processedContent += treeView || generateTreeView(filesInfo);
// Add tree view if requested in options, whether provided or not
if (processingOptions.includeTreeView) {
const resolvedTreeView = treeView || generateTreeView(filesInfo);
if (processingOptions.exportFormat === 'xml') {
processedContent += `<fileStructure>${wrapXmlCdata(resolvedTreeView)}</fileStructure>\n`;
} else {
processedContent += '## File Structure\n\n';
processedContent += '```\n';
processedContent += resolvedTreeView;
processedContent += '```\n\n';
}
}

processedContent += '```\n\n';
processedContent += '## File Contents\n\n';
}
if (processingOptions.exportFormat === 'markdown' && processingOptions.includeTreeView) {
processedContent += '## File Contents\n\n';
}

let totalTokens = 0;
let processedFiles = 0;
let skippedFiles = 0;
if (processingOptions.exportFormat === 'xml') {
processedContent += '<files>\n';
}

for (const fileInfo of filesInfo ?? []) {
try {
if (!fileInfo || !fileInfo.path) {
console.warn('Skipping invalid file info entry');
skippedFiles++;
continue;
}
let totalTokens = 0;
let processedFiles = 0;
let skippedFiles = 0;

const { path: filePath, tokens = 0 } = fileInfo;
for (const fileInfo of filesInfo ?? []) {
try {
if (!fileInfo || !fileInfo.path) {
console.warn('Skipping invalid file info entry');
skippedFiles++;
continue;
}

// Use consistent path joining
const fullPath = path.join(rootPath, filePath);
const filePath = fileInfo.path;
const tokenCount = normalizeTokenCount(fileInfo.tokens);

// Validate the full path is within the root path
const normalizedFullPath = normalizePath(fullPath);
const normalizedRootPath = normalizePath(rootPath);
// Resolve and validate against root path to prevent traversal and prefix bypasses.
const fullPath = path.resolve(rootPath, filePath);

if (!normalizedFullPath.startsWith(normalizedRootPath)) {
console.warn(`Skipping file outside root directory: ${filePath}`);
skippedFiles++;
continue;
}
if (!isPathWithinRoot(rootPath, fullPath)) {
console.warn(`Skipping file outside root directory: ${filePath}`);
skippedFiles++;
continue;
}

if (fs.existsSync(fullPath)) {
const content = contentProcessor.processFile(fullPath, filePath);
if (fs.existsSync(fullPath)) {
const content = contentProcessor.processFile(fullPath, filePath, {
exportFormat: processingOptions.exportFormat,
showTokenCount: processingOptions.showTokenCount,
tokenCount,
});

if (content) {
processedContent += content;
totalTokens += tokens;
processedFiles++;
if (content) {
processedContent += content;
totalTokens += tokenCount;
processedFiles++;
}
} else {
console.warn(`File not found: ${filePath}`);
skippedFiles++;
}
} else {
console.warn(`File not found: ${filePath}`);
} catch (error) {
console.warn(`Failed to process file: ${getErrorMessage(error)}`);
skippedFiles++;
}
} catch (error) {
console.warn(`Failed to process file: ${error.message}`);
skippedFiles++;
}
}

processedContent += '\n--END--\n';
if (processingOptions.exportFormat === 'xml') {
processedContent += '</files>\n';
processedContent +=
`<summary totalTokens="${toXmlNumericAttribute(totalTokens)}" ` +
`processedFiles="${toXmlNumericAttribute(processedFiles)}" ` +
`skippedFiles="${toXmlNumericAttribute(skippedFiles)}" />\n`;
processedContent += '</repositoryContent>\n';
} else {
processedContent += '\n--END--\n';
}

return {
content: processedContent,
totalTokens,
processedFiles,
skippedFiles,
filesInfo: filesInfo, // Add filesInfo to the response
};
} catch (error) {
console.error('Error processing repository:', error);
throw error;
}
return {
content: processedContent,
exportFormat: processingOptions.exportFormat,
totalTokens,
processedFiles,
skippedFiles,
filesInfo: filesInfo, // Add filesInfo to the response
};
} catch (error) {
console.error('Error processing repository:', error);
throw error;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The processedContent string is built using repeated concatenation (+=) inside a loop and across several conditional blocks. In JavaScript, this can be inefficient for a large number of files because strings are immutable, leading to the creation of many intermediate strings. A more performant approach is to collect all the string parts in an array and then join them once at the end.

    try {
      const tokenCounter = new TokenCounter();
      const contentProcessor = new ContentProcessor(tokenCounter);

      // Ensure options is an object with default values if missing
      const processingOptions = {
        showTokenCount: options.showTokenCount !== false, // Default to true if not explicitly false
        includeTreeView: options.includeTreeView === true,
        exportFormat: normalizeExportFormat(options.exportFormat),
      };

      console.log('Processing with options:', processingOptions);

      const contentParts: string[] = [];

      if (processingOptions.exportFormat === 'xml') {
        contentParts.push('<?xml version="1.0" encoding="UTF-8"?>\n');
        contentParts.push('<repositoryContent>\n');
      } else {
        contentParts.push('# Repository Content\n\n');
      }

      // Add tree view if requested in options, whether provided or not
      if (processingOptions.includeTreeView) {
        const resolvedTreeView = treeView || generateTreeView(filesInfo);
        if (processingOptions.exportFormat === 'xml') {
          contentParts.push(`<fileStructure>${wrapXmlCdata(resolvedTreeView)}</fileStructure>\n`);
        } else {
          contentParts.push('## File Structure\n\n');
          contentParts.push('```\n');
          contentParts.push(resolvedTreeView);
          contentParts.push('```\n\n');
        }
      }

      if (processingOptions.exportFormat === 'markdown' && processingOptions.includeTreeView) {
        contentParts.push('## File Contents\n\n');
      }

      if (processingOptions.exportFormat === 'xml') {
        contentParts.push('<files>\n');
      }

      let totalTokens = 0;
      let processedFiles = 0;
      let skippedFiles = 0;

      for (const fileInfo of filesInfo ?? []) {
        try {
          if (!fileInfo || !fileInfo.path) {
            console.warn('Skipping invalid file info entry');
            skippedFiles++;
            continue;
          }

          const filePath = fileInfo.path;
          const tokenCount = normalizeTokenCount(fileInfo.tokens);

          // Resolve and validate against root path to prevent traversal and prefix bypasses.
          const fullPath = path.resolve(rootPath, filePath);

          if (!isPathWithinRoot(rootPath, fullPath)) {
            console.warn(`Skipping file outside root directory: ${filePath}`);
            skippedFiles++;
            continue;
          }

          if (fs.existsSync(fullPath)) {
            const content = contentProcessor.processFile(fullPath, filePath, {
              exportFormat: processingOptions.exportFormat,
              showTokenCount: processingOptions.showTokenCount,
              tokenCount,
            });

            if (content) {
              contentParts.push(content);
              totalTokens += tokenCount;
              processedFiles++;
            }
          } else {
            console.warn(`File not found: ${filePath}`);
            skippedFiles++;
          }
        } catch (error) {
          console.warn(`Failed to process file: ${getErrorMessage(error)}`);
          skippedFiles++;
        }
      }

      if (processingOptions.exportFormat === 'xml') {
        contentParts.push('</files>\n');
        contentParts.push(
          `<summary totalTokens="${toXmlNumericAttribute(totalTokens)}" ` +
            `processedFiles="${toXmlNumericAttribute(processedFiles)}" ` +
            `skippedFiles="${toXmlNumericAttribute(skippedFiles)}" />\n`
        );
        contentParts.push('</repositoryContent>\n');
      } else {
        contentParts.push('\n--END--\n');
      }

      return {
        content: contentParts.join(''),
        exportFormat: processingOptions.exportFormat,
        totalTokens,
        processedFiles,
        skippedFiles,
        filesInfo: filesInfo, // Add filesInfo to the response
      };
    } catch (error) {
      console.error('Error processing repository:', error);
      throw error;
    }

@Mehdi-Bl

Mehdi-Bl commented Feb 9, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up fixes pushed in commits fc7c9af and 82807f6.

What was addressed:

  • Added main-process authorization gate for root-scoped IPC calls and tests for unauthorized root behavior.
  • Added regression coverage for root re-selection and tree connector rendering.
  • Fixed tree rendering connector logic in generateTreeView.
  • Preserved authorization across window reopen lifecycle to avoid localStorage/root mismatch regressions while keeping root-boundary enforcement.
  • Hardened XML e2e test isolation (shared electron dialog mock + handler map reset).

Validation rerun locally:

  • npm run lint
  • npm test -- --runInBand (217 passed)
  • npm run build:ts
  • pre-commit run --files src/main/index.ts tests/integration/main-process/handlers.test.ts tests/integration/main-process/xml-export-e2e.test.ts

Claude local critical review pass was rerun against this patchset; no new blocking critical/high issue was identified beyond the authorization lifecycle tradeoff already addressed above.

@gemini-code-assist please review the latest commits for any remaining critical/high security or regression gaps.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This is an excellent pull request that introduces a valuable XML export feature and, more importantly, significantly hardens the application against path traversal vulnerabilities. The security improvements, such as establishing an authorized root path and implementing robust path validation in the main process, are well-executed and critical for application safety. The XML export functionality is thoughtfully implemented with proper sanitization, escaping, and handling of edge cases. The test coverage for all the new features and security fixes is comprehensive and greatly increases confidence in the changes. I have one minor suggestion in src/main/index.ts to improve code conciseness.

Comment thread src/main/index.ts
Comment on lines +574 to +587
const filters =
defaultExtension === '.xml'
? [
{ name: 'XML Files', extensions: ['xml'] },
{ name: 'Markdown Files', extensions: ['md'] },
{ name: 'Text Files', extensions: ['txt'] },
{ name: 'All Files', extensions: ['*'] },
]
: [
{ name: 'Markdown Files', extensions: ['md'] },
{ name: 'XML Files', extensions: ['xml'] },
{ name: 'Text Files', extensions: ['txt'] },
{ name: 'All Files', extensions: ['*'] },
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The logic for constructing the filters array is a bit repetitive. You can define the filter objects once and then arrange them in the array based on the defaultExtension. This will make the code more concise and easier to maintain if you add more file types in the future.

  const mdFilter = { name: 'Markdown Files', extensions: ['md'] };
  const xmlFilter = { name: 'XML Files', extensions: ['xml'] };
  const txtFilter = { name: 'Text Files', extensions: ['txt'] };
  const allFilter = { name: 'All Files', extensions: ['*'] };

  const filters =
    defaultExtension === '.xml'
      ? [xmlFilter, mdFilter, txtFilter, allFilter]
      : [mdFilter, xmlFilter, txtFilter, allFilter];

@Mehdi-Bl

Mehdi-Bl commented Feb 9, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up pushed: 075f66e to fix Windows QA failure.

Fix details:

  • Updated tests/integration/main-process/handlers.test.ts path mocks to use stable POSIX implementations for resolve, relative, normalize, and isAbsolute.
  • This removes platform-dependent behavior that caused fs:getDirectoryTree authorization assertions to fail on windows-latest.

Validation:

  • npm run lint
  • npm run build:ts
  • npm test -- --runInBand (217 passed)
  • CI is now green across ubuntu/macos/windows + security gates.

@gemini-code-assist please review latest commit 075f66e for any remaining critical/high issues.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces significant new functionality by adding XML export capabilities alongside Markdown, enhancing the tool's versatility. A major focus has been placed on security hardening, particularly against path traversal vulnerabilities, through the implementation of robust path validation across both the main and renderer processes. The new export-format.ts utility centralizes XML-specific logic, promoting code reusability and maintainability. Comprehensive unit and integration tests have been added for the new XML export pipeline and the path validation logic, demonstrating a strong commitment to correctness and reliability. Overall, this is a well-implemented feature with a strong emphasis on security and robustness.

I am having trouble creating individual review comments. Click here to see my feedback.

tests/integration/main-process/handlers.test.ts (376-377)

critical

Adding test cases for prefix collision (/mock/repo-secrets/config.js) and traversal (/mock/repo/../repo-secrets/hidden.js) is excellent. These scenarios are common in path traversal attacks, and explicitly testing their rejection ensures the robustness of the repo:analyze security measures.

tests/integration/main-process/handlers.test.ts (197-205)

critical

This test case is critical for verifying the security hardening of the assets protocol. It explicitly checks that requests attempting path traversal (e.g., ../../etc/passwd) are correctly rejected, preventing unauthorized access to system files.

tests/unit/components/app.test.tsx (318-329)

critical

This test case is critical for verifying the client-side path validation. It ensures that the isPathWithinRootBoundary function correctly rejects file selections that attempt prefix collision or traversal outside the authorized root, preventing potential security vulnerabilities from reaching the main process.

src/main/index.ts (300-303)

critical

Implementing resolveAuthorizedPath at the start of repo:analyze is a critical security measure. This immediately validates the rootPath against the globally authorized path, ensuring that analysis operations are restricted to the intended directory and preventing unauthorized access or information disclosure.

src/main/index.ts (328-332)

critical

This check using isPathWithinRoot is a critical security enhancement. It prevents individual files from being processed if they are outside the authorizedAnalyzeRoot, even if their filePath might appear to be within the root due to malicious path manipulation (e.g., ../).

src/main/index.ts (451-454)

critical

The use of resolveAuthorizedPath at the start of repo:process is a critical security measure. This ensures that all processing operations are confined to the user-selected root directory, preventing path traversal attacks and unauthorized file access.

src/main/index.ts (514-520)

critical

This isPathWithinRoot check is a critical security enhancement. It prevents the processing of individual files that might be outside the authorizedProcessRoot, even if their relative paths are manipulated to appear valid. This protects against path traversal vulnerabilities during content processing.

tests/integration/main-process/handlers.test.ts (928-931)

critical

This test case is critical for verifying the security hardening of tokens:countFiles. It explicitly checks that token counting requests for unauthorized root paths are rejected, preventing access to sensitive system files.

src/main/index.ts (627-630)

critical

Adding isPathWithinRoot validation for assets:getPath is a critical security improvement. This prevents potential path traversal vulnerabilities where an attacker could request assets outside the designated ASSETS_DIR by manipulating the assetName parameter.

src/main/index.ts (653-657)

critical

The use of resolveAuthorizedPath here is a critical security enhancement. It ensures that token counting requests are validated against the authorizedRootPath, preventing unauthorized access to files outside the selected repository for token calculation.

src/main/index.ts (667-671)

critical

This isPathWithinRoot check is a critical security measure. It ensures that each file in the filePaths array is strictly within the authorizedTokensRoot before proceeding with token counting, preventing path traversal attacks during batch token calculations.

src/renderer/components/App.tsx (303-341)

critical

The introduction of normalizePathForBoundaryCheck and isPathWithinRootBoundary functions in the renderer is a critical security enhancement. These functions provide robust client-side path validation, preventing malicious path manipulations (e.g., ../ traversal or prefix collisions) from being passed to the main process, thereby strengthening the application's overall security posture.

src/types/ipc.ts (78-81)

critical

The introduction of CountFilesTokensOptions with a rootPath field is a critical security improvement. This ensures that when requesting token counts for files, the rootPath is explicitly provided and can be validated by the main process, preventing path traversal attacks and unauthorized access to file contents.

tests/integration/main-process/handlers.test.ts (916-926)

critical

This test case is crucial for verifying the path validation logic in tokens:countFiles. It confirms that relative paths within the authorized root are processed, while relative paths attempting traversal outside the root are rejected, reinforcing security.

tests/integration/main-process/handlers.test.ts (904-914)

critical

This test case is critical for verifying the security hardening of tokens:countFiles. It ensures that files outside the authorized root path are correctly skipped for token counting, preventing unauthorized information disclosure.

tests/integration/main-process/handlers.test.ts (433-442)

critical

This test case is critical for verifying the security hardening of repo:analyze. It explicitly checks that analysis requests for unauthorized root paths are rejected, preventing access to sensitive system directories.

tests/integration/main-process/handlers.test.ts (395-405)

critical

This test case is crucial for verifying the path validation logic. It confirms that relative paths within the authorized root are correctly processed, while relative paths attempting traversal outside the root (e.g., ../repo-secrets/hidden.js) are correctly rejected, reinforcing the security of repo:analyze.

tests/integration/main-process/handlers.test.ts (288-291)

critical

This test case is critical for verifying the security hardening of fs:getDirectoryTree. It ensures that requests for directory trees outside the authorizedRootPath are correctly rejected, preventing unauthorized file system enumeration.

src/main/index.ts (150-154)

critical

The use of resolveAuthorizedPath here is a critical security enhancement. It ensures that any request for a directory tree is validated against the authorizedRootPath, preventing unauthorized access to arbitrary file system locations.

src/main/index.ts (141-142)

critical

Setting authorizedRootPath immediately after a directory is selected is a critical security measure. This ensures that all subsequent file system operations are confined to this explicitly authorized root, preventing path traversal attacks.

tests/integration/main-process/handlers.test.ts (695-728)

critical

This test case is crucial for verifying the authorizedRootPath mechanism. It ensures that after a new directory selection, only paths within the newly authorized root are allowed, and previous authorized paths are correctly invalidated. This prevents a time-of-check-to-time-of-use (TOCTOU) vulnerability where an old, potentially malicious, root path could still be used.

tests/integration/main-process/handlers.test.ts (662-681)

critical

This test case is critical for verifying the security hardening of repo:process. It ensures that files attempting path traversal or prefix collision outside the authorized root are correctly skipped, preventing unauthorized processing of sensitive files.

tests/integration/main-process/handlers.test.ts (683-693)

critical

This test case is critical for verifying the security hardening of repo:process. It explicitly checks that processing requests for unauthorized root paths are rejected, preventing access to sensitive system directories.

tests/unit/utils/content-processor.test.ts (82-98)

high

This test case is critical for verifying the XML hardening. It specifically checks that invalid XML code points in attribute values (like \u0001) are correctly sanitized, ensuring the generated XML is well-formed and preventing potential parsing errors or vulnerabilities.

tests/unit/components/app.test.tsx (448-502)

high

This test case is crucial for verifying the end-to-end XML export functionality. It confirms that when the configuration specifies XML export, the processRepository IPC call receives the correct exportFormat option, and the saveFile IPC call uses the appropriate .xml extension for the default path. This ensures the new feature works as expected from the UI to the file system.

tests/unit/utils/export-format.test.ts (1-42)

high

This new unit test file provides excellent coverage for the export-format utility functions. It thoroughly tests normalizeExportFormat, escapeXmlAttribute, sanitizeXmlContent, wrapXmlCdata, normalizeTokenCount, and toXmlNumericAttribute. This is crucial for ensuring the correctness and robustness of the XML generation and sanitization logic, which are key components of the new feature and its security hardening.

tests/unit/utils/content-processor.test.ts (100-116)

high

This test case is crucial for verifying the robustness of CDATA wrapping. It ensures that CDATA end markers (]]>) within the content are correctly escaped and that invalid XML characters are sanitized, preventing premature termination of CDATA blocks and ensuring the integrity of the XML output.

tests/integration/main-process/xml-export-e2e.test.ts (1-110)

high

This new end-to-end test file is an excellent addition. It thoroughly verifies the XML export pipeline, including content sanitization, CDATA wrapping, and summary metrics, ensuring the generated XML is well-formed and secure. This provides strong confidence in the correctness and robustness of the new feature.

src/main/index.ts (280-290)

high

The isPathWithinRoot function is a well-implemented security utility. It correctly resolves and compares paths to ensure that a candidatePath does not traverse outside the rootPath, effectively preventing path traversal vulnerabilities.

src/utils/content-processor.ts (80-92)

medium

This block correctly implements XML output for text files. It includes path and optional tokens attributes, and safely wraps the file content in CDATA. The use of escapeXmlAttribute and wrapXmlCdata ensures the generated XML is well-formed and secure against injection, while normalizeTokenCount handles token value robustness.

tests/unit/components/app.test.tsx (94-99)

medium

Adding a mock button to select an invalid file is a good test setup. This allows for direct testing of the isPathWithinRootBoundary logic in the renderer, ensuring that client-side path validation correctly rejects unauthorized file selections.

tests/unit/components/app.test.tsx (504-531)

medium

This test case is important for ensuring the robustness of the configuration handling. It verifies that showTokenCount defaults to true when not explicitly set in the config, which is a reasonable default behavior and prevents unexpected omissions of token counts in the output.

tests/integration/main-process/handlers.test.ts (43-62)

medium

Using realPath.posix.normalize, realPath.posix.resolve, realPath.posix.relative, and realPath.posix.isAbsolute in the path mock is a significant improvement for test reliability. This ensures that path operations in tests behave consistently across different operating systems (e.g., Windows vs. Linux), which is crucial for accurately testing security-sensitive path validation logic.

tests/unit/components/config-tab.test.tsx (131-150)

medium

These test cases are important for verifying the persistence of the new export_format setting. They ensure that changes to the export format in the UI are correctly saved to the configuration and that the UI initializes correctly when export_format: xml is specified in the config. This confirms the feature's configurability and state management.

tests/unit/utils/content-processor.test.ts (61-79)

medium

This test case is crucial for verifying the new XML export functionality for text files. It ensures that the processFile method correctly formats content into XML, including path and token attributes, and wraps the content in CDATA. It also correctly asserts that the token counter is not called when tokenCount is explicitly provided, demonstrating efficient processing.

tests/unit/components/app.test.tsx (534-585)

medium

This test case is valuable for verifying the consistency of the save operation. It ensures that the saveFile function uses the exportFormat from the processedResult rather than the current configuration, which is correct behavior if the configuration was changed after processing but before saving. This prevents discrepancies between the displayed content and the saved file type.

src/main/index.ts (589-597)

medium

This conditional logic for dialog.showSaveDialog is a good robustness improvement. It correctly handles cases where mainWindow might be null, ensuring the save dialog can still be displayed without crashing the application. Additionally, dynamically setting filters based on the defaultExtension enhances user experience by prioritizing relevant file types.

tests/unit/utils/content-processor.test.ts (143-158)

medium

This test case is important for verifying the XML export functionality for binary files. It ensures that binary files are correctly represented in XML with appropriate attributes like path, binary status, fileType, and sizeKB, and that the descriptive note is wrapped in CDATA.

src/utils/content-processor.ts (49-60)

medium

This block correctly implements XML output for binary files. It includes relevant attributes like path, binary status, fileType, and sizeKB, and safely wraps a descriptive note in CDATA. The use of escapeXmlAttribute and wrapXmlCdata ensures the generated XML is well-formed and secure against injection.

@Mehdi-Bl

Mehdi-Bl commented Feb 9, 2026

Copy link
Copy Markdown
Contributor Author

Final follow-up pushed: bd48062.

Security hardening added:

  • Updated isPathWithinRoot in src/main/index.ts to use realpath resolution when available (with safe fallback) before boundary checks.
  • This strengthens path-boundary validation against symlink-based escapes while preserving behavior for non-existing paths.

Local validation:

  • npm run lint
  • npm run build:ts
  • npm test -- --runInBand (217 passed)

Current PR status: all checks green (QA matrix + security + analysis gates).

@Mehdi-Bl Mehdi-Bl merged commit c7e7761 into main Feb 9, 2026
14 checks passed
@Mehdi-Bl Mehdi-Bl deleted the feat/xml-export-hardening branch February 9, 2026 18:21
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.

1 participant