@@ -1620,6 +1620,193 @@ fn test_install_compare_option() {
1620
1620
. stderr_contains ( "Options --compare and --strip are mutually exclusive" ) ;
1621
1621
}
1622
1622
1623
+ #[ test]
1624
+ #[ cfg( not( target_os = "openbsd" ) ) ]
1625
+ fn test_install_compare_basic ( ) {
1626
+ let scene = TestScenario :: new ( util_name ! ( ) ) ;
1627
+ let at = & scene. fixtures ;
1628
+
1629
+ let source = "source_file" ;
1630
+ let dest = "dest_file" ;
1631
+
1632
+ at. write ( source, "test content" ) ;
1633
+
1634
+ // First install should copy
1635
+ scene
1636
+ . ucmd ( )
1637
+ . args ( & [ "-Cv" , "-m644" , source, dest] )
1638
+ . succeeds ( )
1639
+ . stdout_contains ( format ! ( "'{source}' -> '{dest}'" ) ) ;
1640
+
1641
+ // Second install with same mode should be no-op (compare works)
1642
+ scene
1643
+ . ucmd ( )
1644
+ . args ( & [ "-Cv" , "-m644" , source, dest] )
1645
+ . succeeds ( )
1646
+ . no_stdout ( ) ;
1647
+
1648
+ // Test that compare works correctly when content actually differs
1649
+ let source2 = "source2" ;
1650
+ at. write ( source2, "different content" ) ;
1651
+
1652
+ scene
1653
+ . ucmd ( )
1654
+ . args ( & [ "-Cv" , "-m644" , source2, dest] )
1655
+ . succeeds ( )
1656
+ . stdout_contains ( "removed" )
1657
+ . stdout_contains ( format ! ( "'{source2}' -> '{dest}'" ) ) ;
1658
+
1659
+ // Second install should be no-op since content is now identical
1660
+ scene
1661
+ . ucmd ( )
1662
+ . args ( & [ "-Cv" , "-m644" , source2, dest] )
1663
+ . succeeds ( )
1664
+ . no_stdout ( ) ;
1665
+ }
1666
+
1667
+ #[ test]
1668
+ #[ cfg( not( any( target_os = "openbsd" , target_os = "freebsd" ) ) ) ]
1669
+ fn test_install_compare_special_mode_bits ( ) {
1670
+ let scene = TestScenario :: new ( util_name ! ( ) ) ;
1671
+ let at = & scene. fixtures ;
1672
+
1673
+ let source = "source_file" ;
1674
+ let dest = "dest_file" ;
1675
+
1676
+ at. write ( source, "test content" ) ;
1677
+
1678
+ // Special mode bits - setgid (tests the core bug fix)
1679
+ // When setgid bit is set, -C should be ignored (always copy)
1680
+ // This tests the bug where b.specified_mode.unwrap_or(0) was used instead of b.mode()
1681
+ scene
1682
+ . ucmd ( )
1683
+ . args ( & [ "-Cv" , "-m2755" , source, dest] )
1684
+ . succeeds ( )
1685
+ . stdout_contains ( format ! ( "'{source}' -> '{dest}'" ) ) ;
1686
+
1687
+ // Second install with same setgid mode should ALSO copy (not skip)
1688
+ // because -C option should be ignored when special mode bits are present
1689
+ scene
1690
+ . ucmd ( )
1691
+ . args ( & [ "-Cv" , "-m2755" , source, dest] )
1692
+ . succeeds ( )
1693
+ . stdout_contains ( "removed" )
1694
+ . stdout_contains ( format ! ( "'{source}' -> '{dest}'" ) ) ;
1695
+
1696
+ // Special mode bits - setuid
1697
+ scene
1698
+ . ucmd ( )
1699
+ . args ( & [ "-Cv" , "-m4755" , source, dest] )
1700
+ . succeeds ( )
1701
+ . stdout_contains ( "removed" )
1702
+ . stdout_contains ( format ! ( "'{source}' -> '{dest}'" ) ) ;
1703
+
1704
+ // Second install with setuid should also copy
1705
+ scene
1706
+ . ucmd ( )
1707
+ . args ( & [ "-Cv" , "-m4755" , source, dest] )
1708
+ . succeeds ( )
1709
+ . stdout_contains ( "removed" )
1710
+ . stdout_contains ( format ! ( "'{source}' -> '{dest}'" ) ) ;
1711
+
1712
+ // Special mode bits - sticky bit
1713
+ scene
1714
+ . ucmd ( )
1715
+ . args ( & [ "-Cv" , "-m1755" , source, dest] )
1716
+ . succeeds ( )
1717
+ . stdout_contains ( "removed" )
1718
+ . stdout_contains ( format ! ( "'{source}' -> '{dest}'" ) ) ;
1719
+
1720
+ // Second install with sticky bit should also copy
1721
+ scene
1722
+ . ucmd ( )
1723
+ . args ( & [ "-Cv" , "-m1755" , source, dest] )
1724
+ . succeeds ( )
1725
+ . stdout_contains ( "removed" )
1726
+ . stdout_contains ( format ! ( "'{source}' -> '{dest}'" ) ) ;
1727
+
1728
+ // Back to normal mode - compare should work again
1729
+ scene
1730
+ . ucmd ( )
1731
+ . args ( & [ "-Cv" , "-m644" , source, dest] )
1732
+ . succeeds ( )
1733
+ . stdout_contains ( "removed" )
1734
+ . stdout_contains ( format ! ( "'{source}' -> '{dest}'" ) ) ;
1735
+
1736
+ // Second install with normal mode should be no-op
1737
+ scene
1738
+ . ucmd ( )
1739
+ . args ( & [ "-Cv" , "-m644" , source, dest] )
1740
+ . succeeds ( )
1741
+ . no_stdout ( ) ;
1742
+ }
1743
+
1744
+ #[ test]
1745
+ #[ cfg( not( target_os = "openbsd" ) ) ]
1746
+ fn test_install_compare_group_ownership ( ) {
1747
+ let scene = TestScenario :: new ( util_name ! ( ) ) ;
1748
+ let at = & scene. fixtures ;
1749
+
1750
+ let source = "source_file" ;
1751
+ let dest = "dest_file" ;
1752
+
1753
+ at. write ( source, "test content" ) ;
1754
+
1755
+ let user_group = std:: process:: Command :: new ( "id" )
1756
+ . arg ( "-nrg" )
1757
+ . output ( )
1758
+ . map_or_else (
1759
+ |_| "users" . to_string ( ) ,
1760
+ |output| String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) ,
1761
+ ) ; // fallback group name
1762
+
1763
+ // Install with explicit group
1764
+ scene
1765
+ . ucmd ( )
1766
+ . args ( & [ "-Cv" , "-m664" , "-g" , & user_group, source, dest] )
1767
+ . succeeds ( )
1768
+ . stdout_contains ( format ! ( "'{source}' -> '{dest}'" ) ) ;
1769
+
1770
+ // Install without group - this should detect that no copy is needed
1771
+ // because the file already has the correct group (user's group)
1772
+ scene
1773
+ . ucmd ( )
1774
+ . args ( & [ "-Cv" , "-m664" , source, dest] )
1775
+ . succeeds ( )
1776
+ . no_stdout ( ) ; // Should be no-op if group ownership logic is correct
1777
+ }
1778
+
1779
+ #[ test]
1780
+ #[ cfg( not( target_os = "openbsd" ) ) ]
1781
+ fn test_install_compare_symlink_handling ( ) {
1782
+ let scene = TestScenario :: new ( util_name ! ( ) ) ;
1783
+ let at = & scene. fixtures ;
1784
+
1785
+ let source = "source_file" ;
1786
+ let symlink_dest = "symlink_dest" ;
1787
+ let target_file = "target_file" ;
1788
+
1789
+ at. write ( source, "test content" ) ;
1790
+ at. write ( target_file, "test content" ) ; // Same content to test that symlinks are always replaced
1791
+ at. symlink_file ( target_file, symlink_dest) ;
1792
+
1793
+ // Create a symlink as destination pointing to a different file - should always be replaced
1794
+ scene
1795
+ . ucmd ( )
1796
+ . args ( & [ "-Cv" , "-m644" , source, symlink_dest] )
1797
+ . succeeds ( )
1798
+ . stdout_contains ( "removed" )
1799
+ . stdout_contains ( format ! ( "'{source}' -> '{symlink_dest}'" ) ) ;
1800
+
1801
+ // Even if content would be the same, symlink destination should be replaced
1802
+ // Now symlink_dest is a regular file, so compare should work normally
1803
+ scene
1804
+ . ucmd ( )
1805
+ . args ( & [ "-Cv" , "-m644" , source, symlink_dest] )
1806
+ . succeeds ( )
1807
+ . no_stdout ( ) ; // Now it's a regular file, so compare should work
1808
+ }
1809
+
1623
1810
#[ test]
1624
1811
// Matches part of tests/install/basic-1
1625
1812
fn test_t_exist_dir ( ) {
0 commit comments