Skip to content

Conversation

@maschad
Copy link
Owner

@maschad maschad commented Jun 11, 2025

Summary by CodeRabbit

  • New Features
    • Improved file transfer protocol to explicitly communicate file paths before data transfer, enhancing reliability and error handling.
    • Clear separation between absolute paths for file operations and display paths for the user interface.
  • Bug Fixes
    • Refined selection and download logic to handle root and relative paths correctly in different modes.
  • Refactor
    • Unified error handling and progress tracking for file transfers, and adopted more generic, async file IO interfaces.
  • Tests
    • Added comprehensive async tests for file transfer, including path handling and protocol verification.
  • Chores
    • Updated .gitignore to exclude macOS system files and test directories.

@coderabbitai
Copy link

coderabbitai bot commented Jun 11, 2025

Walkthrough

The updates introduce a clearer distinction between absolute and display paths for file operations and UI, refactor file transfer utilities to use async traits and atomic progress tracking, and enhance the file transfer protocol to explicitly communicate file paths. Extensive tracing and error handling are added, and comprehensive async tests for file transfer are introduced.

Changes

File(s) Change Summary
.gitignore Added test_share/ and .DS_Store to ignore patterns.
src/app.rs Added display_path to DirectoryItem, refined path handling and selection logic, improved tracing and logging, clarified separation of absolute vs. display paths.
src/main.rs Enhanced path handling and logging, updated to use both absolute and display paths in UI and operations, improved download mode logic.
src/service/node.rs Refactored file transfer protocol to send/receive file paths explicitly before data transfer, added error handling and tracing.
src/service/utils.rs Refactored file transfer utilities to use async traits, atomic progress tracking, unified error handling, switched to relative paths, and used Tokio async file APIs.
src/tests.rs Added async tests for file transfer, introduced StreamWrapper for compatibility, updated existing tests for new struct fields and protocol.

Sequence Diagram(s)

sequenceDiagram
    participant Sender as File Sender
    participant Receiver as File Receiver

    Sender->>Receiver: Open file transfer stream
    Sender->>Receiver: Send path length (u64 LE)
    Sender->>Receiver: Send file path (UTF-8 bytes)
    Sender->>Receiver: Send file size (usize LE)
    Sender->>Receiver: Stream file data in chunks
    Receiver->>Sender: Acknowledge (implicit by protocol completion)
Loading

Poem

A hop, a skip, a path anew,
With bytes and logs, we burrow through!
From root to leaf, the files now show,
With streams and tests that softly flow.
Progress tracked, the errors caught—
This rabbit’s code is finely wrought!
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c5eebf and b9c0d0f.

📒 Files selected for processing (6)
  • .gitignore (1 hunks)
  • src/app.rs (11 hunks)
  • src/main.rs (8 hunks)
  • src/service/node.rs (3 hunks)
  • src/service/utils.rs (1 hunks)
  • src/tests.rs (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Format
🔇 Additional comments (14)
.gitignore (1)

5-6: LGTM!

Standard gitignore patterns for test directories and macOS system files.

src/service/node.rs (1)

488-513: Good error handling implementation

The outbound file transfer properly handles errors at each step with appropriate logging.

src/main.rs (3)

282-285: Potential issue with path canonicalization fallback

If canonicalize fails (e.g., file doesn't exist or permissions issue), the fallback returns the original path which might not be absolute. This could lead to inconsistent behavior.

Consider handling the error case more explicitly:

-                        let abs_path = std::fs::canonicalize(path).unwrap_or_else(|_| path.clone());
+                        let abs_path = match std::fs::canonicalize(path) {
+                            Ok(canonical) => canonical,
+                            Err(e) => {
+                                tracing::warn!("Failed to canonicalize path {:?}: {}", path, e);
+                                // Ensure we at least have an absolute path
+                                if path.is_absolute() {
+                                    path.clone()
+                                } else {
+                                    std::env::current_dir().unwrap_or_default().join(path)
+                                }
+                            }
+                        };

373-385: Consistent path handling in download mode

Good implementation of setting both absolute and display paths for UI consistency.

Also applies to: 415-422


239-240: Improved debug logging

Good change to log the full list of items instead of just the count, providing better debugging information.

src/app.rs (2)

37-38: Good separation of concerns for path handling

The distinction between path (for file operations) and display_path (for UI) is a clean design that prevents path-related bugs.


428-437: Good defensive programming

Excellent validation to ensure the path is not empty before using it.

src/tests.rs (3)

14-62: Well-implemented async trait adapter

The StreamWrapper correctly bridges between tokio::io and futures::io async traits, enabling proper testing of the refactored file transfer utilities.


295-370: Comprehensive test coverage for new file transfer protocol

Excellent test coverage including:

  • Path handling verification
  • Full protocol testing with mock streams
  • Both relative and absolute path scenarios

284-284: Test updated correctly for new DirectoryItem structure

The test properly initializes the new display_path field.

src/service/utils.rs (4)

1-10: LGTM! Well-organized imports for async file operations.

The dual import of async traits from both futures and tokio is correctly handled with aliasing to avoid conflicts.


11-38: Well-designed error handling with proper trait implementations.

The FileTransferError enum provides comprehensive error handling for file transfer operations with appropriate trait implementations for error propagation.


73-136: Robust file streaming implementation with proper protocol handling.

The stream_file method correctly implements the file transfer protocol by sending path information and file size before content, with efficient buffered reading and atomic progress tracking.


138-231: Well-implemented file receiving with proper protocol adherence.

The FileReceiver correctly implements the receiving side of the protocol, with proper bounds checking to prevent over-reading and automatic parent directory creation. The atomic progress tracking matches the sender side.

Comment on lines +278 to +303
let mut path_len_bytes = [0u8; 8];
if let Err(e) = stream.read_exact(&mut path_len_bytes).await {
tracing::error!("Failed to read path length from peer {}: {}", peer, e);
drop(permit);
return;
}
let path_len = usize::try_from(u64::from_le_bytes(path_len_bytes))
.map_err(|e| Box::new(e) as Box<dyn Error + Send>);

// Read the requested file path
let mut path_bytes = vec![0u8; path_len.unwrap()];
if let Err(e) = stream.read_exact(&mut path_bytes).await {
tracing::error!("Failed to read path from peer {}: {}", peer, e);
drop(permit);
return;
}
let file_path = match String::from_utf8(path_bytes) {
Ok(path) => path,
Err(e) => {
tracing::error!("Invalid UTF-8 in path from peer {}: {}", peer, e);
drop(permit);
return;
}
};

match receiver.receive_file(&mut stream).await {
Ok(file_name) => {
tracing::info!("Successfully received file '{}' from peer {}", file_name, peer);
tracing::info!("Received file request for '{}' from peer {}", file_path, peer);
Copy link

@coderabbitai coderabbitai bot Jun 11, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Critical security issues: unbounded memory allocation and path traversal vulnerability

The code has two critical security vulnerabilities:

  1. DoS via unbounded memory allocation: Line 288 allocates memory based on untrusted input without bounds checking.
  2. Path traversal vulnerability: Line 306 uses the received path without validation.

Apply this diff to fix both issues:

-                            let path_len = usize::try_from(u64::from_le_bytes(path_len_bytes))
-                                .map_err(|e| Box::new(e) as Box<dyn Error + Send>);
-
-                            // Read the requested file path
-                            let mut path_bytes = vec![0u8; path_len.unwrap()];
+                            let path_len = match usize::try_from(u64::from_le_bytes(path_len_bytes)) {
+                                Ok(len) if len <= 4096 => len, // Reasonable path length limit
+                                Ok(len) => {
+                                    tracing::error!("Path length too large from peer {}: {}", peer, len);
+                                    drop(permit);
+                                    return;
+                                }
+                                Err(e) => {
+                                    tracing::error!("Invalid path length from peer {}: {}", peer, e);
+                                    drop(permit);
+                                    return;
+                                }
+                            };
+
+                            // Read the requested file path
+                            let mut path_bytes = vec![0u8; path_len];

Additionally, validate the path after line 301:

                             };

                             tracing::info!("Received file request for '{}' from peer {}", file_path, peer);
+                            
+                            // Validate path - prevent directory traversal
+                            let path = PathBuf::from(&file_path);
+                            if path.components().any(|c| matches!(c, std::path::Component::ParentDir)) {
+                                tracing::error!("Invalid path with directory traversal from peer {}: {}", peer, file_path);
+                                drop(permit);
+                                return;
+                            }

                             // Send the file
-                            let transfer = FileTransfer::new(&PathBuf::from(&file_path));
+                            let transfer = FileTransfer::new(&path);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let mut path_len_bytes = [0u8; 8];
if let Err(e) = stream.read_exact(&mut path_len_bytes).await {
tracing::error!("Failed to read path length from peer {}: {}", peer, e);
drop(permit);
return;
}
let path_len = usize::try_from(u64::from_le_bytes(path_len_bytes))
.map_err(|e| Box::new(e) as Box<dyn Error + Send>);
// Read the requested file path
let mut path_bytes = vec![0u8; path_len.unwrap()];
if let Err(e) = stream.read_exact(&mut path_bytes).await {
tracing::error!("Failed to read path from peer {}: {}", peer, e);
drop(permit);
return;
}
let file_path = match String::from_utf8(path_bytes) {
Ok(path) => path,
Err(e) => {
tracing::error!("Invalid UTF-8 in path from peer {}: {}", peer, e);
drop(permit);
return;
}
};
match receiver.receive_file(&mut stream).await {
Ok(file_name) => {
tracing::info!("Successfully received file '{}' from peer {}", file_name, peer);
tracing::info!("Received file request for '{}' from peer {}", file_path, peer);
let mut path_len_bytes = [0u8; 8];
if let Err(e) = stream.read_exact(&mut path_len_bytes).await {
tracing::error!("Failed to read path length from peer {}: {}", peer, e);
drop(permit);
return;
}
- let path_len = usize::try_from(u64::from_le_bytes(path_len_bytes))
- .map_err(|e| Box::new(e) as Box<dyn Error + Send>);
-
- // Read the requested file path
- let mut path_bytes = vec![0u8; path_len.unwrap()];
+ let path_len = match usize::try_from(u64::from_le_bytes(path_len_bytes)) {
+ Ok(len) if len <= 4096 => len, // Reasonable path length limit
+ Ok(len) => {
+ tracing::error!("Path length too large from peer {}: {}", peer, len);
+ drop(permit);
+ return;
+ }
+ Err(e) => {
+ tracing::error!("Invalid path length from peer {}: {}", peer, e);
+ drop(permit);
+ return;
+ }
+ };
+
+ // Read the requested file path
+ let mut path_bytes = vec![0u8; path_len];
if let Err(e) = stream.read_exact(&mut path_bytes).await {
tracing::error!("Failed to read path from peer {}: {}", peer, e);
drop(permit);
return;
}
let file_path = match String::from_utf8(path_bytes) {
Ok(path) => path,
Err(e) => {
tracing::error!("Invalid UTF-8 in path from peer {}: {}", peer, e);
drop(permit);
return;
}
};
tracing::info!("Received file request for '{}' from peer {}", file_path, peer);
+ // Validate path — prevent directory traversal
+ let path = PathBuf::from(&file_path);
+ if path.components().any(|c| matches!(c, std::path::Component::ParentDir)) {
+ tracing::error!("Invalid path with directory traversal from peer {}: {}", peer, file_path);
+ drop(permit);
+ return;
+ }
// Send the file
- let transfer = FileTransfer::new(&PathBuf::from(&file_path));
+ let transfer = FileTransfer::new(&path);
🤖 Prompt for AI Agents
In src/service/node.rs around lines 278 to 303, the code reads a path length
from the stream and allocates a vector of that size without any bounds checking,
which can lead to unbounded memory allocation and potential DoS. To fix this,
add a maximum allowed path length check after reading path_len and before
allocating the vector, returning early with an error if the length exceeds this
limit. Additionally, after converting the path bytes to a string, validate the
file path to prevent path traversal attacks by ensuring it does not contain
components like ".." or absolute paths; reject or sanitize invalid paths before
further use.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Open a follow-up GitHub issue for this discussion

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e

TITLE="Security: Validate path length and sanitize file paths in file transfer handler"
PR_URL="https://github.com/maschad/junkanoo/pull/17"
COMMENT_URL="https://github.com/maschad/junkanoo/pull/17#discussion_r2140895560"

BODY=$(cat <<EOF
Follow-up issue for security concerns identified in ${PR_URL}.

Context:
- Discussion comment: ${COMMENT_URL}
- Raised by: @coderabbitai
- Requested by: @maschad

Summary of issues:
1. Potential Denial-of-Service via unbounded memory allocation when allocating buffer for file path length received from peer in \`src/service/node.rs\`.
2. Path traversal vulnerability due to lack of validation/sanitization of the received file path prior to use.

This issue tracks implementing safeguards (reasonable maximum path length, rejection of paths containing parent directory components) and adding tests to cover these cases.
EOF
)

gh issue create --title "$TITLE" --body "$BODY" --assignee "maschad"

Length of output: 1840


@maschad The follow-up security issue has been opened successfully:
#20

It captures the DoS risk from unbounded allocation and the path-traversal vulnerability, and links back to the PR discussion for context. Let me know if you’d like any tweaks to the issue description or additional details!


🧬 Code Graph Analysis Results

Relevant snippets from src/service/utils.rs (lines 48-66 and 73-135)

pub fn new(path: &PathBuf) -> Self {
    // Convert to relative path immediately
    let current_dir = std::env::current_dir().unwrap_or_default();
    let relative_path = path
        .strip_prefix(&current_dir)
        .unwrap_or(path)
        .to_path_buf();

    tracing::debug!(
        "Relative path being used for file transfer: {:?}",
        relative_path
    );

    Self {
        path: relative_path,
        chunk_size: 1024 * 1024, // 1MB chunks
        progress: Arc::new(AtomicUsize::new(0)),
    }
}

pub async fn stream_file<S>(&self, stream: &mut S) -> Result<(), Box<dyn Error + Send>>
where
    S: AsyncWrite + Unpin,
{
    let current_dir =
        std::env::current_dir().map_err(|e| Box::new(e) as Box<dyn Error + Send>)?;
    let full_path = current_dir.join(&self.path);

    tracing::debug!("Full path being used for file transfer: {:?}", full_path);

    let file = File::open(&full_path)
        .await
        .map_err(|e| Box::new(e) as Box<dyn Error + Send>)?;
    let metadata = file
        .metadata()
        .await
        .map_err(|e| Box::new(e) as Box<dyn Error + Send>)?;
    let file_size =
        usize::try_from(metadata.len()).map_err(|e| Box::new(e) as Box<dyn Error + Send>)?;

    // Send the relative path and file size
    let path_str = self.path.to_string_lossy().to_string();
    let path_bytes = path_str.as_bytes();
    stream
        .write_all(&(path_bytes.len() as u64).to_le_bytes())
        .await
        .map_err(|e| Box::new(e) as Box<dyn Error + Send>)?;
    stream
        .write_all(path_bytes)
        .await
        .map_err(|e| Box::new(e) as Box<dyn Error + Send>)?;
    stream
        .write_all(&file_size.to_le_bytes())
        .await
        .map_err(|e| Box::new(e) as Box<dyn Error + Send>)?;

    let mut reader = tokio::io::BufReader::with_capacity(self.chunk_size, file);
    let mut buffer = vec![0u8; self.chunk_size];
    let mut total_read = 0;

    loop {
        let bytes_read = reader
            .read(&mut buffer)
            .await
            .map_err(|e| Box::new(e) as Box<dyn Error + Send>)?;
        if bytes_read == 0 {
            break;
        }

        stream
            .write_all(&buffer[..bytes_read])
            .await
            .map_err(|e| Box::new(e) as Box<dyn Error + Send>)?;
        total_read += bytes_read;
        self.progress.store(total_read, Ordering::SeqCst);
    }

    stream
        .flush()
        .await
        .map_err(|e| Box::new(e) as Box<dyn Error + Send>)?;
    Ok(())
}

Summary:
These functions define a FileTransfer struct constructor and a method to stream a file over an asynchronous writable stream. The new function converts a given path to a relative path based on the current working directory. The stream_file method opens the file, sends the relative path length, path bytes, and file size over the stream, then reads the file in chunks and writes these chunks to the stream asynchronously. It handles errors by boxing them as Box<dyn Error + Send>. Progress is tracked via an atomic counter.


Possibly relevant snippet from src/app.rs (lines 65-91)

pub fn new() -> Self {
    let mut app = Self {
        directory_items: Vec::new(),
        all_shared_items: Vec::new(),
        directory_cache: HashMap::new(),
        selected_index: None,
        current_path: std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
        connection_state: ConnectionState::Disconnected,
        peer_id: PeerId::random(),
        connected_peer_id: None,
        state: AppState::Share,
        is_host: true,
        is_loading: false,
        listening_addrs: Vec::new(),
        items_to_share: HashSet::new(),
        items_being_shared: HashSet::new(),
        items_to_download: HashSet::new(),
        items_being_downloaded: HashSet::new(),
        warning: None,
        refresh_sender: None,
        client: None,
        clipboard_success: false,
    };

    app.populate_directory_items();
    app
}

Summary:
This snippet shows the initialization of the main application state struct, including fields for directory items, connection state, peer IDs, sharing and downloading state, and a client handle. It sets up the initial current directory and peer ID, which are relevant to the network service's peer and directory management.


These snippets provide context on file transfer handling, peer and directory state initialization, and asynchronous streaming operations relevant to the src/service/node.rs file's network and file transfer logic.

Comment on lines 554 to +569
let file_names: Vec<String> = self
.items_to_download
.iter()
.map(|path| path.to_string_lossy().to_string())
.filter_map(|path| {
// Find the original item in all_shared_items to get the absolute path
self.all_shared_items
.iter()
.find(|item| {
item.name == path.file_name().unwrap_or_default().to_string_lossy()
})
.map(|item| {
let path_str = item.path.to_string_lossy().to_string();
tracing::info!("Using absolute path for download: {}", path_str);
path_str
})
})
Copy link

@coderabbitai coderabbitai bot Jun 11, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential issue with file name matching

The current implementation matches files by name only, which could select the wrong file if multiple files have the same name in different directories.

Consider using a more robust matching strategy:

-                    .find(|item| {
-                        item.name == path.file_name().unwrap_or_default().to_string_lossy()
-                    })
+                    .find(|item| {
+                        // Match by the full path or a unique identifier
+                        &item.path == path || 
+                        (item.name == path.file_name().unwrap_or_default().to_string_lossy() 
+                         && item.path.parent() == path.parent())
+                    })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let file_names: Vec<String> = self
.items_to_download
.iter()
.map(|path| path.to_string_lossy().to_string())
.filter_map(|path| {
// Find the original item in all_shared_items to get the absolute path
self.all_shared_items
.iter()
.find(|item| {
item.name == path.file_name().unwrap_or_default().to_string_lossy()
})
.map(|item| {
let path_str = item.path.to_string_lossy().to_string();
tracing::info!("Using absolute path for download: {}", path_str);
path_str
})
})
let file_names: Vec<String> = self
.items_to_download
.iter()
.filter_map(|path| {
// Find the original item in all_shared_items to get the absolute path
self.all_shared_items
.iter()
.find(|item| {
// Match by the full path or a unique identifier
&item.path == path
|| (item.name == path.file_name().unwrap_or_default().to_string_lossy()
&& item.path.parent() == path.parent())
})
.map(|item| {
let path_str = item.path.to_string_lossy().to_string();
tracing::info!("Using absolute path for download: {}", path_str);
path_str
})
})
🤖 Prompt for AI Agents
In src/app.rs around lines 554 to 569, the code matches files by name only when
filtering items_to_download against all_shared_items, which can cause incorrect
matches if multiple files share the same name in different directories. To fix
this, update the matching logic to compare the full relative or absolute paths
instead of just the file names, ensuring that the correct file is selected by
uniquely identifying it with its complete path.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Open a follow-up GitHub issue for this discussion

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

@maschad Sure thing — I’ll open a follow-up issue that captures the file-matching concern raised in the review discussion.


🏁 Script executed:

#!/bin/bash
# Create a GitHub issue documenting the path-matching bug discussed in PR #17.
gh issue create \
  --title "Follow-up: Improve file matching when resolving download paths" \
  --body "$(cat <<'EOF'
### Context
During code review of pull request #17, a potential bug was identified in `src/app.rs` (see comment https://github.com/maschad/junkanoo/pull/17#discussion_r2140895565).  
The current implementation resolves files to download by matching **only the file name**, which can select the wrong file when multiple shared files have identical names in different directories.

### Expected
Files chosen for download should be matched unambiguously, e.g. by comparing full relative/absolute paths or another unique identifier.

### Impact
If duplicate file names exist in separate directories, users may receive the wrong file.

### Acceptance Criteria
- Matching logic uniquely identifies the intended file (full path or equivalent).
- Unit/integration tests cover scenarios with duplicate file names.

Referenced PR: https://github.com/maschad/junkanoo/pull/17
EOF
)" \
  --assignee "maschad"

Length of output: 1036


@maschad ✅ Follow-up issue created: #18 — it captures the file-matching bug noted in the review and includes context, expected behaviour, impact, and acceptance criteria. Let me know if you’d like any tweaks or extra details added.


🧬 Code Graph Analysis Results

Relevant snippets from src/service/node.rs (lines 55-126)

pub fn new() -> Result<(Client, impl Stream<Item = Event>, EventLoop, PeerId), Box<dyn Error>> {
    // Create a public/private key pair, either random or based on a seed.
    // let id_keys = Keypair::generate_ed25519();
    // let peer_id = id_keys.public().to_peer_id();
    let peer_id = PeerId::random();

    let mut swarm = SwarmBuilder::with_new_identity()
        .with_tokio()
        .with_tcp(
            tcp::Config::default(),
            noise::Config::new,
            yamux::Config::default,
        )?
        .with_quic()
        .with_behaviour(|key| Behaviour {
            kademlia: kad::Behaviour::new(
                peer_id,
                kad::store::MemoryStore::new(key.public().to_peer_id()),
            ),
            request_response: request_response::cbor::Behaviour::new(
                [(JUNKANOO_REQUEST_RESPONSE_PROTOCOL, ProtocolSupport::Full)],
                request_response::Config::default(),
            ),
            file_stream: stream::Behaviour::new(),
        })?
        .with_swarm_config(|c| {
            c.with_idle_connection_timeout(Duration::from_secs(CONNECTION_TIMEOUT))
        })
        .build();

    // Set up file transfer protocol listener
    let incoming_streams = swarm
        .behaviour_mut()
        .file_stream
        .new_control()
        .accept(JUNKANOO_FILE_PROTOCOL)
        .unwrap();
    tracing::info!(
        "Listening for file transfer streams on protocol: {}",
        JUNKANOO_FILE_PROTOCOL
    );

    // Set Kademlia into server mode before adding bootnodes
    swarm
        .behaviour_mut()
        .kademlia
        .set_mode(Some(kad::Mode::Server));

    // Then add the bootnodes
    for peer in &BOOTNODES {
        if let Ok(peer_id) = peer.parse() {
            swarm
                .behaviour_mut()
                .kademlia
                .add_address(&peer_id, "/dnsaddr/bootstrap.libp2p.io".parse()?);
        }
    }

    let (command_sender, command_receiver) = mpsc::channel(0);
    let (event_sender, event_receiver) = mpsc::channel(0);

    let local_peer_id = *swarm.local_peer_id();

    Ok((
        Client {
            sender: command_sender,
        },
        event_receiver,
        EventLoop::new(swarm, command_receiver, event_sender, incoming_streams),
        local_peer_id,
    ))
}

Relevant snippet from src/main.rs (lines 352-464)

async fn handle_download_mode(
    client: &mut Client,
    target_peer_addr: Multiaddr,
    app: Arc<Mutex<App>>,
) -> Result<(), &'static str> {
    let target_peer_id = target_peer_addr
        .iter()
        .find_map(|p| match p {
            Protocol::P2p(peer_id) => Some(peer_id),
            _ => None,
        })
        .ok_or("Peer address must contain a peer ID component (/p2p/...)")?;

    client.dial(target_peer_id, target_peer_addr).await.unwrap();
    tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;

    // Initial directory request
    match client.request_directory(target_peer_id).await {
        Ok(display_response) => {
            let mut items = display_response.items;
            // For download mode, we need to ensure both paths are properly set
            for item in &mut items {
                tracing::info!("path before setting paths is: {:?}", item.path);
                // Keep the absolute path for file operations
                let abs_path = item.path.clone();
                // Use just the name for display
                let display_path = PathBuf::from(&item.name);
                item.path = abs_path;
                item.display_path = display_path;
                tracing::info!(
                    "Set paths - absolute: {:?}, display: {:?}",
                    item.path,
                    item.display_path
                );
            }
            items.sort_by(|a, b| match (a.is_dir, b.is_dir) {
                (true, false) => std::cmp::Ordering::Less,
                (false, true) => std::cmp::Ordering::Greater,
                _ => match a.depth.cmp(&b.depth) {
                    std::cmp::Ordering::Equal => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
                    other => other,
                },
            });

            {
                let mut app = app.lock();
                app.all_shared_items.clone_from(&items);
                app.directory_items = items;
                app.current_path = PathBuf::new();
                app.populate_directory_items();
                tracing::info!("Initial directory items: {:?}", app.directory_items);
            }

            // Start a background task to handle directory updates
            let mut client_clone = client.clone();
            let app_clone = app.clone();
            tokio::spawn(async move {
                loop {
                    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
                    match client_clone.request_directory(target_peer_id).await {
                        Ok(display_response) => {
                            let mut items = display_response.items;
                            // For download mode, we need to ensure both paths are properly set
                            for item in &mut items {
                                // Keep the absolute path for file operations
                                let abs_path = item.path.clone();
                                // Use just the name for display
                                let display_path = PathBuf::from(&item.name);
                                item.path = abs_path;
                                item.display_path = display_path;
                            }
                            items.sort_by(|a, b| match (a.is_dir, b.is_dir) {
                                (true, false) => std::cmp::Ordering::Less,
                                (false, true) => std::cmp::Ordering::Greater,
                                _ => match a.depth.cmp(&b.depth) {
                                    std::cmp::Ordering::Equal => {
                                        a.name.to_lowercase().cmp(&b.name.to_lowercase())
                                    }
                                    other => other,
                                },
                            });

                            let mut app = app_clone.lock();
                            if app.all_shared_items != items {
                                app.all_shared_items.clone_from(&items);
                                app.directory_items = items;
                                app.populate_directory_items();
                                tracing::info!(
                                    "Updated directory items: {:?}",
                                    app.directory_items
                                );
                                // Notify UI to refresh
                                if let Some(refresh_sender) = &app.refresh_sender {
                                    let _ = refresh_sender.try_send(());
                                }
                            }
                        }
                        Err(e) => {
                            tracing::error!("Failed to request directory: {}", e);
                            break;
                        }
                    }
                }
            });

            Ok(())
        }
        Err(e) => {
            tracing::error!("Failed to request directory: {}", e);
            Err("Failed to request directory")
        }
    }
}

Summary of relevant code in src/app.rs (the provided file)

  • The App struct manages application state including directory items, shared/downloaded items, connection state, and UI state.
  • It supports two modes: AppState::Share and AppState::Download.
  • The method populate_directory_items populates the directory listing based on the current path and mode.
  • The method handle_download_mode filters and sorts directory items when in download mode, showing items from all_shared_items.
  • The select_item and unselect_item methods manage selection of files/directories for sharing or downloading, storing relative paths for sharing and absolute paths for downloading.
  • The start_download async method initiates file download requests via the Client using absolute paths resolved from all_shared_items.
  • The App maintains a client: Option<Client> to communicate with peers.
  • The refresh_sender is used to notify the UI to refresh when selections or directory items change.

These snippets and summaries provide context on how the app manages file sharing and downloading, peer connections, directory listings, and UI updates, which are relevant for understanding the file and any related discussions or issues.

Comment on lines +48 to 66
pub fn new(path: &PathBuf) -> Self {
// Convert to relative path immediately
let current_dir = std::env::current_dir().unwrap_or_default();
let relative_path = path
.strip_prefix(&current_dir)
.unwrap_or(path)
.to_path_buf();

tracing::debug!(
"Relative path being used for file transfer: {:?}",
relative_path
);

Self {
path: metadata.path.into(),
path: relative_path,
chunk_size: 1024 * 1024, // 1MB chunks
progress: 0,
progress: Arc::new(AtomicUsize::new(0)),
}
}
Copy link

@coderabbitai coderabbitai bot Jun 11, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider explicit error handling for current directory retrieval.

The use of unwrap_or_default() on line 50 could mask errors when getting the current directory. If the current directory cannot be determined, using an empty path as default might lead to unexpected behavior.

Consider propagating the error or using a more explicit fallback:

-pub fn new(path: &PathBuf) -> Self {
+pub fn new(path: &PathBuf) -> Result<Self, std::io::Error> {
     // Convert to relative path immediately
-    let current_dir = std::env::current_dir().unwrap_or_default();
+    let current_dir = std::env::current_dir()?;
     let relative_path = path
         .strip_prefix(&current_dir)
         .unwrap_or(path)
         .to_path_buf();

     tracing::debug!(
         "Relative path being used for file transfer: {:?}",
         relative_path
     );

-    Self {
+    Ok(Self {
         path: relative_path,
         chunk_size: 1024 * 1024, // 1MB chunks
         progress: Arc::new(AtomicUsize::new(0)),
-    }
+    })
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn new(path: &PathBuf) -> Self {
// Convert to relative path immediately
let current_dir = std::env::current_dir().unwrap_or_default();
let relative_path = path
.strip_prefix(&current_dir)
.unwrap_or(path)
.to_path_buf();
tracing::debug!(
"Relative path being used for file transfer: {:?}",
relative_path
);
Self {
path: metadata.path.into(),
path: relative_path,
chunk_size: 1024 * 1024, // 1MB chunks
progress: 0,
progress: Arc::new(AtomicUsize::new(0)),
}
}
pub fn new(path: &PathBuf) -> Result<Self, std::io::Error> {
// Convert to relative path immediately
let current_dir = std::env::current_dir()?;
let relative_path = path
.strip_prefix(&current_dir)
.unwrap_or(path)
.to_path_buf();
tracing::debug!(
"Relative path being used for file transfer: {:?}",
relative_path
);
Ok(Self {
path: relative_path,
chunk_size: 1024 * 1024, // 1MB chunks
progress: Arc::new(AtomicUsize::new(0)),
})
}
🤖 Prompt for AI Agents
In src/service/utils.rs around lines 48 to 66, the current implementation uses
unwrap_or_default() when retrieving the current directory, which can mask errors
and lead to unexpected behavior if the directory cannot be determined. Modify
the new function to explicitly handle the error from std::env::current_dir() by
either propagating the error using a Result return type or by implementing a
clear fallback strategy with proper error logging or handling, instead of
defaulting to an empty path.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Open a follow-up GitHub issue for this discussion

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

@maschad Sure thing — I’ll open a follow-up issue that captures this refactor task and links back to the PR discussion.


🏁 Script executed:

#!/bin/bash
# Create a GitHub issue to track explicit error handling in FileTransfer::new
gh issue create \
  --title "Refactor: handle std::env::current_dir() errors in FileTransfer::new" \
  --body $'In PR #17 (fix: correct paths being passed to fix streaming bug), CodeRabbit flagged that `FileTransfer::new` currently calls `std::env::current_dir().unwrap_or_default()`, which can silently fall back to an empty path and mask OS errors.\n\nThis issue tracks replacing that `unwrap_or_default()` usage with proper error propagation (e.g. returning `Result<Self, std::io::Error>` or another explicit strategy).\n\nReference discussion:\nhttps://github.com/maschad/junkanoo/pull/17#discussion_r2140895570\n\nRequested by @maschad.' \
  --assignee maschad

Length of output: 693


@maschad Done! 📌 I’ve opened the follow-up issue and assigned it to you:

#19

Let me know if there’s anything else you’d like tweaked or tracked.


🧬 Code Graph Analysis Results

Relevant snippets from src/service/utils.rs (lines 1-174)

The entire file content is relevant as it defines the core file transfer utilities used in the service layer:

  • FileTransferError enum: Defines error types for file transfer operations, including IO and UTF-8 errors, with implementations for Display, Error, and conversions.
  • FileTransfer struct and impl:
    • Fields: path (relative path), chunk_size (default 1MB), progress (atomic counter).
    • new(path: &PathBuf) -> Self: Creates a new FileTransfer instance, converting the given path to a relative path based on the current directory.
    • stream_file<S>(&self, stream: &mut S) -> Result<(), Box<dyn Error + Send>>: Asynchronously streams the file contents over the provided async write stream. It sends the relative path length, path bytes, and file size before streaming the file in chunks. Progress is tracked via an atomic counter.
  • FileReceiver struct and impl:
    • Fields: chunk_size (default 1MB), progress (atomic counter).
    • new() -> Self: Creates a new FileReceiver.
    • receive_file<S>(&self, stream: &mut S) -> Result<String, Box<dyn Error + Send>>: Asynchronously receives a file from the provided async read/write stream. It reads the relative path length, path bytes, and file size, creates necessary directories, writes the file in chunks, and tracks progress. Returns the relative path as a String.

These utilities handle the low-level details of file transfer over asynchronous streams, including error handling, chunked reading/writing, and path management.


Possibly relevant snippets from src/app.rs (lines 65-91)

pub fn new() -> Self {
    let mut app = Self {
        directory_items: Vec::new(),
        all_shared_items: Vec::new(),
        directory_cache: HashMap::new(),
        selected_index: None,
        current_path: std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
        connection_state: ConnectionState::Disconnected,
        peer_id: PeerId::random(),
        connected_peer_id: None,
        state: AppState::Share,
        is_host: true,
        is_loading: false,
        listening_addrs: Vec::new(),
        items_to_share: HashSet::new(),
        items_being_shared: HashSet::new(),
        items_to_download: HashSet::new(),
        items_being_downloaded: HashSet::new(),
        warning: None,
        refresh_sender: None,
        client: None,
        clipboard_success: false,
    };

    app.populate_directory_items();
    app
}

This snippet shows the initialization of the main application state, including current directory, peer IDs, connection state, and collections related to sharing and downloading items. It may be relevant for understanding how file transfers are integrated into the app's state.


Possibly relevant snippets from src/service/node.rs (lines 55-126)

pub fn new() -> Result<(Client, impl Stream<Item = Event>, EventLoop, PeerId), Box<dyn Error>> {
    // Create a public/private key pair, either random or based on a seed.
    // let id_keys = Keypair::generate_ed25519();
    // let peer_id = id_keys.public().to_peer_id();
    let peer_id = PeerId::random();

    let mut swarm = SwarmBuilder::with_new_identity()
        .with_tokio()
        .with_tcp(
            tcp::Config::default(),
            noise::Config::new,
            yamux::Config::default(),
        )?
        .with_quic()
        .with_behaviour(|key| Behaviour {
            kademlia: kad::Behaviour::new(
                peer_id,
                kad::store::MemoryStore::new(key.public().to_peer_id()),
            ),
            request_response: request_response::cbor::Behaviour::new(
                [(JUNKANOO_REQUEST_RESPONSE_PROTOCOL, ProtocolSupport::Full)],
                request_response::Config::default(),
            ),
            file_stream: stream::Behaviour::new(),
        })?
        .with_swarm_config(|c| {
            c.with_idle_connection_timeout(Duration::from_secs(CONNECTION_TIMEOUT))
        })
        .build();

    // Set up file transfer protocol listener
    let incoming_streams = swarm
        .behaviour_mut()
        .file_stream
        .new_control()
        .accept(JUNKANOO_FILE_PROTOCOL)
        .unwrap();
    tracing::info!(
        "Listening for file transfer streams on protocol: {}",
        JUNKANOO_FILE_PROTOCOL
    );

    // Set Kademlia into server mode before adding bootnodes
    swarm
        .behaviour_mut()
        .kademlia
        .set_mode(Some(kad::Mode::Server));

    // Then add the bootnodes
    for peer in &BOOTNODES {
        if let Ok(peer_id) = peer.parse() {
            swarm
                .behaviour_mut()
                .kademlia
                .add_address(&peer_id, "/dnsaddr/bootstrap.libp2p.io".parse()?);
        }
    }

    let (command_sender, command_receiver) = mpsc::channel(0);
    let (event_sender, event_receiver) = mpsc::channel(0);

    let local_peer_id = *swarm.local_peer_id();

    Ok((
        Client {
            sender: command_sender,
        },
        event_receiver,
        EventLoop::new(swarm, command_receiver, event_sender, incoming_streams),
        local_peer_id,
    ))
}

This snippet shows the creation of the network node, including setting up the libp2p swarm with file transfer protocol support. It is relevant for understanding how the file transfer utilities might be used in the network context.


Possibly relevant snippets from src/service/node.rs (lines 243-259)

fn new(
    swarm: Swarm<Behaviour>,
    command_receiver: mpsc::Receiver<Command>,
    event_sender: mpsc::Sender<Event>,
    incoming_streams: stream::IncomingStreams,
) -> Self {
    Self {
        swarm,
        command_receiver,
        event_sender,
        pending_dial: HashMap::default(),
        pending_request_file: HashMap::default(),
        pending_request_display: HashMap::default(),
        pending_directory_items: HashMap::default(),
        incoming_streams,
    }
}

This snippet shows the initialization of the event loop handling the swarm and incoming streams, which likely includes handling file transfer streams. It is relevant for understanding the integration of file transfer in the node's event loop.


No other snippets appear directly relevant to the file transfer utilities or their usage context.

@maschad maschad merged commit 3313958 into main Jun 11, 2025
3 checks passed
@maschad maschad deleted the mc/fix/not-downloading-release branch June 11, 2025 19:18
@Mustaphamuhammadddj
Copy link

Mustapha Muhammad from Nigeria Sokoto Tambuwal Dogon daji Akalawa Area 850102
+2348107778089
IMG_20250407_143357_020

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.

3 participants