@@ -1694,6 +1694,193 @@ fn test_install_compare_option() {
16941694 . stderr_contains ( "Options --compare and --strip are mutually exclusive" ) ;
16951695}
16961696
1697+ #[ test]
1698+ #[ cfg( not( target_os = "openbsd" ) ) ]
1699+ fn test_install_compare_basic ( ) {
1700+ let scene = TestScenario :: new ( util_name ! ( ) ) ;
1701+ let at = & scene. fixtures ;
1702+
1703+ let source = "source_file" ;
1704+ let dest = "dest_file" ;
1705+
1706+ at. write ( source, "test content" ) ;
1707+
1708+ // First install should copy
1709+ scene
1710+ . ucmd ( )
1711+ . args ( & [ "-Cv" , "-m644" , source, dest] )
1712+ . succeeds ( )
1713+ . stdout_contains ( format ! ( "'{source}' -> '{dest}'" ) ) ;
1714+
1715+ // Second install with same mode should be no-op (compare works)
1716+ scene
1717+ . ucmd ( )
1718+ . args ( & [ "-Cv" , "-m644" , source, dest] )
1719+ . succeeds ( )
1720+ . no_stdout ( ) ;
1721+
1722+ // Test that compare works correctly when content actually differs
1723+ let source2 = "source2" ;
1724+ at. write ( source2, "different content" ) ;
1725+
1726+ scene
1727+ . ucmd ( )
1728+ . args ( & [ "-Cv" , "-m644" , source2, dest] )
1729+ . succeeds ( )
1730+ . stdout_contains ( "removed" )
1731+ . stdout_contains ( format ! ( "'{source2}' -> '{dest}'" ) ) ;
1732+
1733+ // Second install should be no-op since content is now identical
1734+ scene
1735+ . ucmd ( )
1736+ . args ( & [ "-Cv" , "-m644" , source2, dest] )
1737+ . succeeds ( )
1738+ . no_stdout ( ) ;
1739+ }
1740+
1741+ #[ test]
1742+ #[ cfg( not( any( target_os = "openbsd" , target_os = "freebsd" ) ) ) ]
1743+ fn test_install_compare_special_mode_bits ( ) {
1744+ let scene = TestScenario :: new ( util_name ! ( ) ) ;
1745+ let at = & scene. fixtures ;
1746+
1747+ let source = "source_file" ;
1748+ let dest = "dest_file" ;
1749+
1750+ at. write ( source, "test content" ) ;
1751+
1752+ // Special mode bits - setgid (tests the core bug fix)
1753+ // When setgid bit is set, -C should be ignored (always copy)
1754+ // This tests the bug where b.specified_mode.unwrap_or(0) was used instead of b.mode()
1755+ scene
1756+ . ucmd ( )
1757+ . args ( & [ "-Cv" , "-m2755" , source, dest] )
1758+ . succeeds ( )
1759+ . stdout_contains ( format ! ( "'{source}' -> '{dest}'" ) ) ;
1760+
1761+ // Second install with same setgid mode should ALSO copy (not skip)
1762+ // because -C option should be ignored when special mode bits are present
1763+ scene
1764+ . ucmd ( )
1765+ . args ( & [ "-Cv" , "-m2755" , source, dest] )
1766+ . succeeds ( )
1767+ . stdout_contains ( "removed" )
1768+ . stdout_contains ( format ! ( "'{source}' -> '{dest}'" ) ) ;
1769+
1770+ // Special mode bits - setuid
1771+ scene
1772+ . ucmd ( )
1773+ . args ( & [ "-Cv" , "-m4755" , source, dest] )
1774+ . succeeds ( )
1775+ . stdout_contains ( "removed" )
1776+ . stdout_contains ( format ! ( "'{source}' -> '{dest}'" ) ) ;
1777+
1778+ // Second install with setuid should also copy
1779+ scene
1780+ . ucmd ( )
1781+ . args ( & [ "-Cv" , "-m4755" , source, dest] )
1782+ . succeeds ( )
1783+ . stdout_contains ( "removed" )
1784+ . stdout_contains ( format ! ( "'{source}' -> '{dest}'" ) ) ;
1785+
1786+ // Special mode bits - sticky bit
1787+ scene
1788+ . ucmd ( )
1789+ . args ( & [ "-Cv" , "-m1755" , source, dest] )
1790+ . succeeds ( )
1791+ . stdout_contains ( "removed" )
1792+ . stdout_contains ( format ! ( "'{source}' -> '{dest}'" ) ) ;
1793+
1794+ // Second install with sticky bit should also copy
1795+ scene
1796+ . ucmd ( )
1797+ . args ( & [ "-Cv" , "-m1755" , source, dest] )
1798+ . succeeds ( )
1799+ . stdout_contains ( "removed" )
1800+ . stdout_contains ( format ! ( "'{source}' -> '{dest}'" ) ) ;
1801+
1802+ // Back to normal mode - compare should work again
1803+ scene
1804+ . ucmd ( )
1805+ . args ( & [ "-Cv" , "-m644" , source, dest] )
1806+ . succeeds ( )
1807+ . stdout_contains ( "removed" )
1808+ . stdout_contains ( format ! ( "'{source}' -> '{dest}'" ) ) ;
1809+
1810+ // Second install with normal mode should be no-op
1811+ scene
1812+ . ucmd ( )
1813+ . args ( & [ "-Cv" , "-m644" , source, dest] )
1814+ . succeeds ( )
1815+ . no_stdout ( ) ;
1816+ }
1817+
1818+ #[ test]
1819+ #[ cfg( not( target_os = "openbsd" ) ) ]
1820+ fn test_install_compare_group_ownership ( ) {
1821+ let scene = TestScenario :: new ( util_name ! ( ) ) ;
1822+ let at = & scene. fixtures ;
1823+
1824+ let source = "source_file" ;
1825+ let dest = "dest_file" ;
1826+
1827+ at. write ( source, "test content" ) ;
1828+
1829+ let user_group = std:: process:: Command :: new ( "id" )
1830+ . arg ( "-nrg" )
1831+ . output ( )
1832+ . map_or_else (
1833+ |_| "users" . to_string ( ) ,
1834+ |output| String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) ,
1835+ ) ; // fallback group name
1836+
1837+ // Install with explicit group
1838+ scene
1839+ . ucmd ( )
1840+ . args ( & [ "-Cv" , "-m664" , "-g" , & user_group, source, dest] )
1841+ . succeeds ( )
1842+ . stdout_contains ( format ! ( "'{source}' -> '{dest}'" ) ) ;
1843+
1844+ // Install without group - this should detect that no copy is needed
1845+ // because the file already has the correct group (user's group)
1846+ scene
1847+ . ucmd ( )
1848+ . args ( & [ "-Cv" , "-m664" , source, dest] )
1849+ . succeeds ( )
1850+ . no_stdout ( ) ; // Should be no-op if group ownership logic is correct
1851+ }
1852+
1853+ #[ test]
1854+ #[ cfg( not( target_os = "openbsd" ) ) ]
1855+ fn test_install_compare_symlink_handling ( ) {
1856+ let scene = TestScenario :: new ( util_name ! ( ) ) ;
1857+ let at = & scene. fixtures ;
1858+
1859+ let source = "source_file" ;
1860+ let symlink_dest = "symlink_dest" ;
1861+ let target_file = "target_file" ;
1862+
1863+ at. write ( source, "test content" ) ;
1864+ at. write ( target_file, "test content" ) ; // Same content to test that symlinks are always replaced
1865+ at. symlink_file ( target_file, symlink_dest) ;
1866+
1867+ // Create a symlink as destination pointing to a different file - should always be replaced
1868+ scene
1869+ . ucmd ( )
1870+ . args ( & [ "-Cv" , "-m644" , source, symlink_dest] )
1871+ . succeeds ( )
1872+ . stdout_contains ( "removed" )
1873+ . stdout_contains ( format ! ( "'{source}' -> '{symlink_dest}'" ) ) ;
1874+
1875+ // Even if content would be the same, symlink destination should be replaced
1876+ // Now symlink_dest is a regular file, so compare should work normally
1877+ scene
1878+ . ucmd ( )
1879+ . args ( & [ "-Cv" , "-m644" , source, symlink_dest] )
1880+ . succeeds ( )
1881+ . no_stdout ( ) ; // Now it's a regular file, so compare should work
1882+ }
1883+
16971884#[ test]
16981885// Matches part of tests/install/basic-1
16991886fn test_t_exist_dir ( ) {
0 commit comments