Skip to content

💀 Deprecate fPRM Permission API; superseded by owner facet chunks#3071

Merged
ChanTsune merged 9 commits into
mainfrom
lib/deprecate-fprm
May 23, 2026
Merged

💀 Deprecate fPRM Permission API; superseded by owner facet chunks#3071
ChanTsune merged 9 commits into
mainfrom
lib/deprecate-fprm

Conversation

@ChanTsune
Copy link
Copy Markdown
Owner

@ChanTsune ChanTsune commented May 17, 2026

Summary by CodeRabbit

  • Deprecated

    • Legacy Permission API and fPRM chunk marked deprecated (since 0.34.0); EntryBuilder.permission(...) is deprecated—use owner_* fields + permission_mode.
  • Changed behavior

    • Read/write now use explicit owner_* fields and permission_mode; ownership resolution prefers owner facets and may emit blank owner/group when absent.
    • Commands (chmod/chown/extract/list/strip/migrate/etc.) operate on owner facets/permission_mode instead of the legacy Permission object.
    • Owner names/SIDs and permission_mode are preserved and owner names truncated to safe UTF‑8/255‑byte limits.
  • New Features

    • Migration/transform tools convert legacy permission data into owner facets.
  • Tests

    • Extensive tests added/updated to validate owner_* fields, SIDs, truncation, and permission_mode behavior.

Review Change Stack

@github-actions github-actions Bot added the lib This issue is about lib crate label May 17, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 17, 2026

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

Deprecate legacy fPRM/Permission and replace uses with ResolvedOwnership and explicit owner-facet fields; update EntryBuilder, conversion paths, CLI commands, pna add_metadata translation, archive writer, and tests accordingly.

Changes

Owner-facet migration and Permission deprecation

Layer / File(s) Summary
Full migration & deprecation
lib/..., cli/..., pna/..., xtask/..., cli/tests/...
Marks ChunkType::fPRM and Permission APIs deprecated, adds ResolvedOwnership, replaces Permission-based flows with owner-facet setters/getters and permission_mode, updates xtask tar/zip conversion and pna::EntryBuilderExt::add_metadata, updates archive writer to emit owner-facet chunks, and updates many tests to use the new metadata accessors.

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

Possibly related PRs

"I'm a rabbit in a burrow bright,
I swapped old perms for owner light,
fPRM sleeps through the night,
New facets make metadata right 🥕"

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately reflects the main objective: deprecating the fPRM Permission API in favor of owner facet chunks, which is the central theme across all file changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lib/deprecate-fprm

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.

Copy link
Copy Markdown
Contributor

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

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 deprecates the fPRM chunk and the Permission struct in favor of new owner facet chunks (fUId, fGId, fONm, fGNm, fOSi, fGSi, fMOd). The changes include adding #[deprecated] attributes to the relevant types and methods in EntryBuilder and Metadata, along with #[allow(deprecated)] annotations to internal implementations and tests to maintain compatibility. No review comments were provided, so I have no feedback to offer regarding the review process.

Copy link
Copy Markdown
Contributor

@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.

Caution

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

⚠️ Outside diff range comments (2)
lib/src/archive/write.rs (1)

531-585: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Serialize owner-facet chunks in write_file_entry.

Line 569 writes deprecated fPRM, but the function never emits fUId/fGId/fONm/fGNm/fOSi/fGSi/fMOd. This silently drops metadata written via the replacement API when callers use Archive::write_file / SolidArchive::write_file.

💡 Suggested patch
 pub(crate) fn write_file_entry<W, F>(
@@
     if let Some(p) = metadata.permission {
         (ChunkType::fPRM, p.to_bytes()).write_chunk_in(inner)?;
     }
+    if let Some(v) = metadata.owner_uid {
+        (ChunkType::fUId, v.to_bytes()).write_chunk_in(inner)?;
+    }
+    if let Some(v) = metadata.owner_gid {
+        (ChunkType::fGId, v.to_bytes()).write_chunk_in(inner)?;
+    }
+    if let Some(v) = metadata.owner_user_name {
+        (ChunkType::fONm, v.to_bytes()).write_chunk_in(inner)?;
+    }
+    if let Some(v) = metadata.owner_group_name {
+        (ChunkType::fGNm, v.to_bytes()).write_chunk_in(inner)?;
+    }
+    if let Some(v) = metadata.owner_user_sid {
+        (ChunkType::fOSi, v.to_bytes()).write_chunk_in(inner)?;
+    }
+    if let Some(v) = metadata.owner_group_sid {
+        (ChunkType::fGSi, v.to_bytes()).write_chunk_in(inner)?;
+    }
+    if let Some(v) = metadata.permission_mode {
+        (ChunkType::fMOd, v.to_bytes()).write_chunk_in(inner)?;
+    }
+    if let Some(v) = metadata.link_target_type {
+        (ChunkType::fLTP, v.to_bytes()).write_chunk_in(inner)?;
+    }
     let context = get_writer_context(option)?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/src/archive/write.rs` around lines 531 - 585, write_file_entry currently
writes deprecated fPRM but omits the owner-related chunks (fUId, fGId, fONm,
fGNm, fOSi, fGSi, fMOd), so owner/facet metadata is dropped; update
write_file_entry to check metadata.owner fields and emit the corresponding
chunks (ChunkType::fUId, ::fGId, ::fONm, ::fGNm, ::fOSi, ::fGSi, ::fMOd) using
the same .write_chunk_in(inner)? pattern (and appropriate .to_bytes()
conversions or byte slices) — place these writes alongside the permission write
(after the fPRM block) so callers of Archive::write_file /
SolidArchive::write_file preserve owner user id, group id, owner/group names,
sids and mode. Ensure you reference the function write_file_entry, the metadata
variable, and the ChunkType enum names when adding the emissions.
pna/src/ext/entry_builder.rs (1)

54-62: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

add_metadata should copy owner facets and permission_mode.

Line 60 only forwards deprecated permission; owner-facet fields are not copied, so metadata migration paths can lose data when this helper is used.

💡 Suggested patch
     fn add_metadata(&mut self, metadata: &Metadata) -> &mut Self {
         self.created(metadata.created())
             .modified(metadata.modified())
             .accessed(metadata.accessed())
             .permission(metadata.permission().cloned())
+            .owner_uid(metadata.owner_uid())
+            .owner_gid(metadata.owner_gid())
+            .owner_user_name(metadata.owner_user_name().cloned())
+            .owner_group_name(metadata.owner_group_name().cloned())
+            .owner_user_sid(metadata.owner_user_sid().cloned())
+            .owner_group_sid(metadata.owner_group_sid().cloned())
+            .permission_mode(metadata.permission_mode())
             .link_target_type(metadata.link_target_type())
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pna/src/ext/entry_builder.rs` around lines 54 - 62, add_metadata currently
only forwards the deprecated permission() and omits owner facets and the new
permission_mode, causing data loss; update the add_metadata helper to also set
the new permission_mode (call .permission_mode(metadata.permission_mode())) and
copy owner facet fields from Metadata (e.g.
.owner_user(metadata.owner_user().cloned()) and
.owner_group(metadata.owner_group().cloned()) or the appropriate owner-* builder
methods your EntryBuilder exposes), and retain or map the deprecated
.permission(metadata.permission().cloned()) only for backward compatibility.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@lib/src/archive/write.rs`:
- Around line 531-585: write_file_entry currently writes deprecated fPRM but
omits the owner-related chunks (fUId, fGId, fONm, fGNm, fOSi, fGSi, fMOd), so
owner/facet metadata is dropped; update write_file_entry to check metadata.owner
fields and emit the corresponding chunks (ChunkType::fUId, ::fGId, ::fONm,
::fGNm, ::fOSi, ::fGSi, ::fMOd) using the same .write_chunk_in(inner)? pattern
(and appropriate .to_bytes() conversions or byte slices) — place these writes
alongside the permission write (after the fPRM block) so callers of
Archive::write_file / SolidArchive::write_file preserve owner user id, group id,
owner/group names, sids and mode. Ensure you reference the function
write_file_entry, the metadata variable, and the ChunkType enum names when
adding the emissions.

In `@pna/src/ext/entry_builder.rs`:
- Around line 54-62: add_metadata currently only forwards the deprecated
permission() and omits owner facets and the new permission_mode, causing data
loss; update the add_metadata helper to also set the new permission_mode (call
.permission_mode(metadata.permission_mode())) and copy owner facet fields from
Metadata (e.g. .owner_user(metadata.owner_user().cloned()) and
.owner_group(metadata.owner_group().cloned()) or the appropriate owner-* builder
methods your EntryBuilder exposes), and retain or map the deprecated
.permission(metadata.permission().cloned()) only for backward compatibility.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c5829cb4-503a-45d7-bebd-fd42b8799468

📥 Commits

Reviewing files that changed from the base of the PR and between 9206da6 and 43bd85c.

📒 Files selected for processing (7)
  • lib/src/archive.rs
  • lib/src/archive/write.rs
  • lib/src/chunk/types.rs
  • lib/src/entry.rs
  • lib/src/entry/builder.rs
  • lib/src/entry/meta.rs
  • pna/src/ext/entry_builder.rs

@github-actions github-actions Bot added the cli This issue is about cli application label May 18, 2026
Copy link
Copy Markdown
Contributor

@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: 5

🧹 Nitpick comments (1)
xtask/src/main.rs (1)

522-532: 💤 Low value

Consider setting default permissions when unix_mode() is absent.

When unix_mode() returns None (common for ZIP files created on Windows), no ownership or permission metadata is set on the entry. This preserves accuracy but may leave entries with no permissions in the resulting archive.

This is likely acceptable for a conversion tool, but worth noting that Windows-originated ZIPs will have entries without permission metadata.

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

In `@xtask/src/main.rs` around lines 522 - 532, The apply_zip_owner function
currently only sets owner and permission metadata when entry.unix_mode() is
Some, leaving entries from Windows ZIPs without any owner/permission metadata;
add an else branch in apply_zip_owner (referencing builder and
entry.unix_mode()) to set sensible defaults: set owner_uid and owner_gid to 0
(libpna::OwnerUid::from(0) and libpna::OwnerGid::from(0)) and set
permission_mode to a reasonable default based on whether the entry is a
directory (e.g., 0o755) or a file (e.g., 0o644) — detect directories via
entry.is_dir() or entry.name().ends_with('/') and call
builder.permission_mode(libpna::PermissionMode::from(...)) accordingly so
Windows-originated entries get predictable permissions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/src/command/chown.rs`:
- Around line 111-149: The code currently synthesizes uid/gid = 0 by using
own.uid.unwrap_or(0) / own.gid.unwrap_or(0) when owner.user/owner.group is None;
change these matches to return Option<u64> (and Option<String> for names)
instead of raw u64 so we preserve absence instead of inventing root. Update the
owner.user match branches to produce uid_opt: Some(u64::MAX) for Name, Some(id)
for ID, Some(sys.uid) for System (if present) and None for the None case unless
own.uid.is_some(); do the same for group -> gid_opt; then call
metadata.with_owner_uid(uid_opt.map(pna::OwnerUid::from)) and
metadata.with_owner_gid(gid_opt.map(pna::OwnerGid::from)) and similarly pass
Option<String> through owner_user_name/group_name helper functions so no 0 is
written when the source lacks owner facets.

In `@cli/src/command/core.rs`:
- Around line 2090-2116: The current branch only applies owner overrides when
source ownership exists (uses own.is_empty()), which prevents
--uid/--gid/--uname/--gname from taking effect for entries without owner facets;
change the conditional to trigger whenever any override is supplied (replace the
if condition to check uid.is_some() || gid.is_some() || uname.is_some() ||
gname.is_some()) so you always build new_uid/new_gid/new_uname/new_gname using
the provided values or falling back to own.*, and then call
metadata.with_owner_uid(...).with_owner_gid(...).with_owner_user_name(...).with_owner_group_name(...).with_permission_mode(own.mode.map(...)).with_owner_user_sid(...).with_owner_group_sid(...
) exactly as before (using ResolvedOwnership::from_metadata for fallback).

In `@cli/src/command/extract.rs`:
- Around line 1605-1619: The code currently skips calling restore_owner when
ResolvedOwnership::from_metadata(item.metadata()) is empty, which prevents
explicit extraction overrides (--uid/--gid/--uname/--gname) from being applied;
change the logic so restore_owner(path, &ownership, options) is invoked whenever
keep_options.owner_strategy is OwnerStrategy::Preserve (i.e., when the user
requested preservation/overrides), regardless of ownership.is_empty(), while
still keeping the mode restoration branch conditional on ownership.mode (leave
the existing DataKind::SymbolicLink and ownership.mode checks intact); update
the block around ResolvedOwnership::from_metadata, the ownership.is_empty()
guard, and the OwnerStrategy::Preserve match to always call restore_owner when
preserve is selected so override-only restores work.

In `@cli/src/command/list.rs`:
- Around line 825-840: The code builds uname/gname by treating missing ownership
fields as present and rendering uid/gid 0; change the logic in the uname/gname
construction (the tuple around row.ownership and variable o) to first detect
whether any ownership info exists (e.g., o.uname.is_some() or o.uid.is_some()
for user, and o.gname.is_some() or o.gid.is_some() for group) and return
Cow::default() when none exist; otherwise, when options.numeric_owner is true
only format and emit the numeric uid/gid if o.uid/o.gid is Some (do not fall
back to 0), and when numeric_owner is false prefer o.uname/o.gname if Some else
fall back to formatting the numeric only if present—update the conditional
expressions around o.uname, o.uid, o.gname, and o.gid accordingly so unknown
ownership is rendered as empty instead of "0".

In `@cli/tests/cli/stdio/option_same_permissions.rs`:
- Around line 673-677: The assertions incorrectly treat uid/gid == 0 as missing;
update the checks to accept any Some(...) by replacing
m.owner_uid().is_some_and(|u| u.get() > 0) with m.owner_uid().is_some() and
similarly replace m.owner_gid().is_some_and(|g| g.get() > 0) with
m.owner_gid().is_some() so the owner presence uses .is_some() alongside
m.owner_user_name().is_some() and m.owner_group_name().is_some().

---

Nitpick comments:
In `@xtask/src/main.rs`:
- Around line 522-532: The apply_zip_owner function currently only sets owner
and permission metadata when entry.unix_mode() is Some, leaving entries from
Windows ZIPs without any owner/permission metadata; add an else branch in
apply_zip_owner (referencing builder and entry.unix_mode()) to set sensible
defaults: set owner_uid and owner_gid to 0 (libpna::OwnerUid::from(0) and
libpna::OwnerGid::from(0)) and set permission_mode to a reasonable default based
on whether the entry is a directory (e.g., 0o755) or a file (e.g., 0o644) —
detect directories via entry.is_dir() or entry.name().ends_with('/') and call
builder.permission_mode(libpna::PermissionMode::from(...)) accordingly so
Windows-originated entries get predictable permissions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 465d5637-3ba6-45fa-934f-b920710cb3d9

📥 Commits

Reviewing files that changed from the base of the PR and between 43bd85c and 2f3b555.

📒 Files selected for processing (43)
  • cli/src/command/acl.rs
  • cli/src/command/chmod.rs
  • cli/src/command/chown.rs
  • cli/src/command/core.rs
  • cli/src/command/core/mtree.rs
  • cli/src/command/core/permission.rs
  • cli/src/command/diff.rs
  • cli/src/command/extract.rs
  • cli/src/command/list.rs
  • cli/src/command/strip.rs
  • cli/src/ext.rs
  • cli/tests/cli/chmod.rs
  • cli/tests/cli/chmod/edge_cases.rs
  • cli/tests/cli/chmod/glob_pattern.rs
  • cli/tests/cli/chmod/keep_solid.rs
  • cli/tests/cli/chmod/multiple_clauses.rs
  • cli/tests/cli/chmod/numeric.rs
  • cli/tests/cli/chmod/password.rs
  • cli/tests/cli/chmod/password_file.rs
  • cli/tests/cli/chmod/symbolic_all.rs
  • cli/tests/cli/chmod/symbolic_combined.rs
  • cli/tests/cli/chmod/symbolic_group.rs
  • cli/tests/cli/chmod/symbolic_other.rs
  • cli/tests/cli/chmod/symbolic_user.rs
  • cli/tests/cli/chmod/unsolid.rs
  • cli/tests/cli/chown/empty_files.rs
  • cli/tests/cli/chown/group_only.rs
  • cli/tests/cli/chown/option_no_owner_lookup.rs
  • cli/tests/cli/chown/option_no_owner_lookup_numeric.rs
  • cli/tests/cli/chown/user_and_group.rs
  • cli/tests/cli/chown/user_only.rs
  • cli/tests/cli/create/numeric_owner.rs
  • cli/tests/cli/create/symlink_metadata.rs
  • cli/tests/cli/create/user_group.rs
  • cli/tests/cli/extract/option_keep_permission.rs
  • cli/tests/cli/list/trailing_slash.rs
  • cli/tests/cli/stdio/mtree.rs
  • cli/tests/cli/stdio/option_same_permissions.rs
  • cli/tests/cli/stdio/symlink.rs
  • cli/tests/cli/strip.rs
  • cli/tests/cli/utils/archive.rs
  • pna/src/ext/entry_builder.rs
  • xtask/src/main.rs

Comment thread cli/src/command/chown.rs
Comment thread cli/src/command/core.rs
Comment on lines +2090 to +2116
let own = crate::ext::ResolvedOwnership::from_metadata(&metadata);
if (uid.is_some() || gid.is_some() || uname.is_some() || gname.is_some()) && !own.is_empty()
{
let new_perm = pna::Permission::new(
uid.map(u64::from).unwrap_or_else(|| perm.uid()),
uname.clone().unwrap_or_else(|| perm.uname().to_string()),
gid.map(u64::from).unwrap_or_else(|| perm.gid()),
gname.clone().unwrap_or_else(|| perm.gname().to_string()),
perm.permissions(),
);
metadata = metadata.with_permission(Some(new_perm));
let new_uid = uid.map(u64::from).or(own.uid);
let new_gid = gid.map(u64::from).or(own.gid);
let new_uname = uname
.clone()
.or_else(|| own.uname.clone())
.unwrap_or_default();
let new_gname = gname
.clone()
.or_else(|| own.gname.clone())
.unwrap_or_default();
metadata = metadata
.with_owner_uid(new_uid.map(pna::OwnerUid::from))
.with_owner_gid(new_gid.map(pna::OwnerGid::from))
.with_owner_user_name(crate::command::core::permission::owner_name_opt(&new_uname))
.with_owner_group_name(crate::command::core::permission::owner_group_name_opt(
&new_gname,
))
.with_permission_mode(own.mode.map(pna::PermissionMode::from))
.with_owner_user_sid(own.user_sid.clone().map(|s| {
pna::OwnerUserSid::new(s).expect("rescued sid within owner-facet bound")
}))
.with_owner_group_sid(own.group_sid.clone().map(|s| {
pna::OwnerGroupSid::new(s).expect("rescued sid within owner-facet bound")
}));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Owner overrides shouldn't depend on source ownership being present.

The !own.is_empty() check makes --uid / --gid / --uname / --gname ineffective when importing archive entries that were created without owner facets. This path already has enough information to synthesize the requested facets, so it should be gated on “override supplied” only and use own just for fallback values.

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

In `@cli/src/command/core.rs` around lines 2090 - 2116, The current branch only
applies owner overrides when source ownership exists (uses own.is_empty()),
which prevents --uid/--gid/--uname/--gname from taking effect for entries
without owner facets; change the conditional to trigger whenever any override is
supplied (replace the if condition to check uid.is_some() || gid.is_some() ||
uname.is_some() || gname.is_some()) so you always build
new_uid/new_gid/new_uname/new_gname using the provided values or falling back to
own.*, and then call
metadata.with_owner_uid(...).with_owner_gid(...).with_owner_user_name(...).with_owner_group_name(...).with_permission_mode(own.mode.map(...)).with_owner_user_sid(...).with_owner_group_sid(...
) exactly as before (using ResolvedOwnership::from_metadata for fallback).

Comment thread cli/src/command/extract.rs Outdated
Comment thread cli/src/command/list.rs Outdated
Comment thread cli/tests/cli/stdio/option_same_permissions.rs Outdated
Copy link
Copy Markdown
Contributor

@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: 1

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

Inline comments:
In `@cli/src/command/migrate.rs`:
- Around line 117-124: The code calls OwnerUserSid::new and OwnerGroupSid::new
inside the map and uses expect, which can panic on malformed archive metadata;
change these to validate and return an io::Error instead of panicking: replace
the .map(|s| pna::OwnerUserSid::new(s).expect(...)) and the group equivalent
with code that maps the Option through OwnerUserSid::new / OwnerGroupSid::new to
a Result and, on Err, converts the error into a std::io::Error (e.g.,
ErrorKind::InvalidData) and returns it from the enclosing function (the
migration path that builds the value passed into with_owner_user_sid and
with_owner_group_sid) so malformed SID values produce a propagated io::Error
rather than panic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c2890525-868e-4fe3-9b8c-3def0e38cd67

📥 Commits

Reviewing files that changed from the base of the PR and between 2f3b555 and be6878f.

📒 Files selected for processing (7)
  • cli/src/command/core.rs
  • cli/src/command/migrate.rs
  • cli/tests/cli/migrate.rs
  • cli/tests/cli/migrate/fprm_to_owner_facet.rs
  • cli/tests/cli/stdio.rs
  • cli/tests/cli/stdio/archive_source_uid_override.rs
  • cli/tests/cli/stdio/symlink.rs
💤 Files with no reviewable changes (1)
  • cli/tests/cli/stdio/symlink.rs
✅ Files skipped from review due to trivial changes (2)
  • cli/tests/cli/stdio.rs
  • cli/tests/cli/migrate.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • cli/src/command/core.rs

Comment on lines +117 to +124
.with_owner_user_sid(
own.user_sid
.map(|s| pna::OwnerUserSid::new(s).expect("rescued sid within owner-facet bound")),
)
.with_owner_group_sid(
own.group_sid
.map(|s| pna::OwnerGroupSid::new(s).expect("rescued sid within owner-facet bound")),
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid panic on malformed SID values during migration.

Line 119 and Line 123 use expect(...) on data derived from archive metadata. A malformed input can panic the process; this path should return an io::Error instead.

Suggested non-panicking fix
-    let new_meta = pna::Metadata::new()
+    let owner_user_sid = own
+        .user_sid
+        .map(pna::OwnerUserSid::new)
+        .transpose()
+        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
+    let owner_group_sid = own
+        .group_sid
+        .map(pna::OwnerGroupSid::new)
+        .transpose()
+        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
+
+    let new_meta = pna::Metadata::new()
         .with_created(created)
         .with_modified(modified)
         .with_accessed(accessed)
         .with_link_target_type(link_target_type)
@@
-        .with_owner_user_sid(
-            own.user_sid
-                .map(|s| pna::OwnerUserSid::new(s).expect("rescued sid within owner-facet bound")),
-        )
-        .with_owner_group_sid(
-            own.group_sid
-                .map(|s| pna::OwnerGroupSid::new(s).expect("rescued sid within owner-facet bound")),
-        );
+        .with_owner_user_sid(owner_user_sid)
+        .with_owner_group_sid(owner_group_sid);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/command/migrate.rs` around lines 117 - 124, The code calls
OwnerUserSid::new and OwnerGroupSid::new inside the map and uses expect, which
can panic on malformed archive metadata; change these to validate and return an
io::Error instead of panicking: replace the .map(|s|
pna::OwnerUserSid::new(s).expect(...)) and the group equivalent with code that
maps the Option through OwnerUserSid::new / OwnerGroupSid::new to a Result and,
on Err, converts the error into a std::io::Error (e.g., ErrorKind::InvalidData)
and returns it from the enclosing function (the migration path that builds the
value passed into with_owner_user_sid and with_owner_group_sid) so malformed SID
values produce a propagated io::Error rather than panic.

Copy link
Copy Markdown
Contributor

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
cli/src/command/chmod.rs (1)

100-132: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Allow numeric chmod to create a mode from scratch.

Lines 103-105 currently bail out before applying Mode::Numeric, so chmod 644 does nothing on entries that don't already carry fMOd or legacy permission metadata. Only symbolic clauses need a current mode.

Suggested fix
 fn transform_entry<T>(entry: NormalEntry<T>, mode: &Mode) -> NormalEntry<T> {
     let metadata = entry.metadata().clone();
     let own = crate::ext::ResolvedOwnership::from_metadata(&metadata);
-    let Some(cur_mode) = own.mode else {
-        return entry.with_metadata(metadata);
-    };
-    let new_mode = mode.apply_to(cur_mode);
+    let new_mode = match (mode, own.mode) {
+        (Mode::Numeric(mode), _) => *mode,
+        (Mode::Clause(_), Some(cur_mode)) => mode.apply_to(cur_mode),
+        (Mode::Clause(_), None) => return entry.with_metadata(metadata),
+    };
     let metadata = metadata
         .with_permission(None)
         .with_owner_uid(own.uid.map(pna::OwnerUid::from))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/command/chmod.rs` around lines 100 - 132, In transform_entry, don't
unconditionally return when own.mode is None; instead detect whether the
requested Mode is purely symbolic (e.g., Mode variants that need a base mode)
and only bail out in that case—if the requested Mode is numeric (Mode::Numeric
or equivalent) build new_mode directly from the numeric value and continue to
set metadata; update the early-return logic around own.mode and Mode to allow
creating a mode from scratch for numeric modes while preserving the current
behavior for symbolic Mode variants in the transform_entry function.
♻️ Duplicate comments (1)
cli/src/command/chown.rs (1)

109-151: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't short-circuit chown on metadata-less entries.

Line 112 still returns the entry unchanged when it has no existing owner/mode metadata, which makes pna experimental chown ... a no-op instead of materializing the requested owner facets on fresh entries.

Suggested fix
 fn transform_entry<T>(entry: NormalEntry<T>, owner: &Ownership) -> NormalEntry<T> {
     let metadata = entry.metadata().clone();
     let own = crate::ext::ResolvedOwnership::from_metadata(&metadata);
-    if own.is_empty() {
-        return entry.with_metadata(metadata);
-    }
     let (uid, uname): (Option<u64>, String) = match owner.user.as_ref() {
         Some(OwnerSpecifier::Name(uname)) => (Some(u64::MAX), uname.clone()),
         Some(OwnerSpecifier::ID(uid)) => (Some(*uid), String::new()),
         Some(OwnerSpecifier::System(user)) => (
             Some(user.uid().unwrap_or(u64::MAX)),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/command/chown.rs` around lines 109 - 151, The transformer currently
returns early in transform_entry when
ResolvedOwnership::from_metadata(&metadata) is empty, making chown a no-op for
entries without existing owner/mode; remove that early return and always build
new metadata so requested owner facets are materialized on fresh entries. Keep
using the same owner-spec matching logic (owner.user / owner.group) to compute
uid/uname and gid/gname, but when own is empty treat
own.uid/own.gid/own.uname/own.gname/own.mode/own.* as absent (None/default) so
the .with_owner_* and .with_permission_mode calls set values from the owner arg
or None as appropriate; update transform_entry (and its use of metadata, own,
uid/gid, uname/gname) to always call entry.with_metadata(new_metadata) instead
of returning early.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/src/command/extract.rs`:
- Around line 1609-1612: The new branch calls restore_owner(...) for
"override-only" cases but resolve_owner still synthesizes missing uid/gid to 0,
causing unintended root assignment; change the logic so restore_owner is only
invoked when the target identity actually exists (i.e., archive or CLI provided
a UID/GID) by updating should_restore_owner or the check around
OwnerStrategy::Preserve to require presence (Some) of the specific ownership
side rather than relying on resolve_owner's 0 fallback, or alternatively modify
resolve_owner to return None for missing uid/gid instead of 0 so restore_owner
receives None and skips applying root — ensure changes reference
OwnerStrategy::Preserve, keep_options.owner_strategy, should_restore_owner,
restore_owner, and resolve_owner so the untouched side remains None unless
explicitly supplied.

---

Outside diff comments:
In `@cli/src/command/chmod.rs`:
- Around line 100-132: In transform_entry, don't unconditionally return when
own.mode is None; instead detect whether the requested Mode is purely symbolic
(e.g., Mode variants that need a base mode) and only bail out in that case—if
the requested Mode is numeric (Mode::Numeric or equivalent) build new_mode
directly from the numeric value and continue to set metadata; update the
early-return logic around own.mode and Mode to allow creating a mode from
scratch for numeric modes while preserving the current behavior for symbolic
Mode variants in the transform_entry function.

---

Duplicate comments:
In `@cli/src/command/chown.rs`:
- Around line 109-151: The transformer currently returns early in
transform_entry when ResolvedOwnership::from_metadata(&metadata) is empty,
making chown a no-op for entries without existing owner/mode; remove that early
return and always build new metadata so requested owner facets are materialized
on fresh entries. Keep using the same owner-spec matching logic (owner.user /
owner.group) to compute uid/uname and gid/gname, but when own is empty treat
own.uid/own.gid/own.uname/own.gname/own.mode/own.* as absent (None/default) so
the .with_owner_* and .with_permission_mode calls set values from the owner arg
or None as appropriate; update transform_entry (and its use of metadata, own,
uid/gid, uname/gname) to always call entry.with_metadata(new_metadata) instead
of returning early.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5eefe8f3-bb8d-4d50-a835-25abbf2a0387

📥 Commits

Reviewing files that changed from the base of the PR and between 2c51a4f and 3a88d53.

📒 Files selected for processing (9)
  • cli/src/command/chmod.rs
  • cli/src/command/chown.rs
  • cli/src/command/core/permission.rs
  • cli/src/command/extract.rs
  • cli/src/ext.rs
  • cli/tests/cli/chmod.rs
  • cli/tests/cli/chown/user_only.rs
  • lib/src/archive/write.rs
  • xtask/src/main.rs
💤 Files with no reviewable changes (1)
  • xtask/src/main.rs

Comment thread cli/src/command/extract.rs
@ChanTsune ChanTsune force-pushed the lib/deprecate-fprm branch 2 times, most recently from 0ba40f6 to a4f8c75 Compare May 23, 2026 02:11
@ChanTsune ChanTsune force-pushed the lib/deprecate-fprm branch from a4f8c75 to 79fd5ea Compare May 23, 2026 02:52
Comment thread cli/tests/cli/strip.rs Fixed
@ChanTsune ChanTsune force-pushed the lib/deprecate-fprm branch from ac30413 to f810c33 Compare May 23, 2026 06:37
@ChanTsune ChanTsune merged commit a6537f0 into main May 23, 2026
141 of 143 checks passed
@ChanTsune ChanTsune deleted the lib/deprecate-fprm branch May 23, 2026 12:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli This issue is about cli application lib This issue is about lib crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants