]> git.ipfire.org Git - thirdparty/rsync.git/commitdiff
chmod: support permission copy modes
authorZen Dodd <mail@steadytao.com>
Sat, 6 Jun 2026 02:57:26 +0000 (12:57 +1000)
committerAndrew Tridgell <andrew@tridgell.net>
Tue, 9 Jun 2026 04:18:29 +0000 (14:18 +1000)
chmod.c
rsync.1.md
testsuite/chmod-option_test.py

diff --git a/chmod.c b/chmod.c
index 8bbf791aabb6cb70c7ed569a4f5bae5e129b94ab..8538d182b0014956218cbde3177b520212ef3e07 100644 (file)
--- a/chmod.c
+++ b/chmod.c
@@ -29,7 +29,7 @@ extern mode_t orig_umask;
 
 struct chmod_mode_struct {
        struct chmod_mode_struct *next;
-       int ModeAND, ModeOR;
+       int ModeAND, ModeOR, ModeCOPY_SRC, ModeCOPY_DST, ModeCOPY_AND, ModeOP;
        char flags;
 };
 
@@ -50,13 +50,13 @@ struct chmod_mode_struct *parse_chmod(const char *modestr,
                                      struct chmod_mode_struct **root_mode_ptr)
 {
        int state = STATE_1ST_HALF;
-       int where = 0, what = 0, op = 0, topbits = 0, topoct = 0, flags = 0;
+       int where = 0, what = 0, op = 0, topbits = 0, topoct = 0, flags = 0, copybits = 0;
        struct chmod_mode_struct *first_mode = NULL, *curr_mode = NULL,
                                 *prev_mode = NULL;
 
        while (state != STATE_ERROR) {
                if (!*modestr || *modestr == ',') {
-                       int bits;
+                       int bits, where_specified;
 
                        if (!op) {
                                state = STATE_ERROR;
@@ -70,9 +70,10 @@ struct chmod_mode_struct *parse_chmod(const char *modestr,
                                first_mode = curr_mode;
                        curr_mode->next = NULL;
 
-                       if (where)
+                       where_specified = where;
+                       if (where) {
                                bits = where * what;
-                       else {
+                       else {
                                where = 0111;
                                bits = (where * what) & ~orig_umask;
                        }
@@ -81,18 +82,34 @@ struct chmod_mode_struct *parse_chmod(const char *modestr,
                        case CHMOD_ADD:
                                curr_mode->ModeAND = CHMOD_BITS;
                                curr_mode->ModeOR  = bits + topoct;
+                               curr_mode->ModeCOPY_SRC = copybits;
+                               curr_mode->ModeCOPY_DST = where;
+                               curr_mode->ModeCOPY_AND = where_specified ? CHMOD_BITS : ~orig_umask;
+                               curr_mode->ModeOP = op;
                                break;
                        case CHMOD_SUB:
                                curr_mode->ModeAND = CHMOD_BITS - bits - topoct;
                                curr_mode->ModeOR  = 0;
+                               curr_mode->ModeCOPY_SRC = copybits;
+                               curr_mode->ModeCOPY_DST = where;
+                               curr_mode->ModeCOPY_AND = where_specified ? CHMOD_BITS : ~orig_umask;
+                               curr_mode->ModeOP = op;
                                break;
                        case CHMOD_EQ:
                                curr_mode->ModeAND = CHMOD_BITS - (where * 7) - (topoct ? topbits : 0);
                                curr_mode->ModeOR  = bits + topoct;
+                               curr_mode->ModeCOPY_SRC = copybits;
+                               curr_mode->ModeCOPY_DST = where;
+                               curr_mode->ModeCOPY_AND = where_specified ? CHMOD_BITS : ~orig_umask;
+                               curr_mode->ModeOP = op;
                                break;
                        case CHMOD_SET:
                                curr_mode->ModeAND = 0;
                                curr_mode->ModeOR  = bits;
+                               curr_mode->ModeCOPY_SRC = 0;
+                               curr_mode->ModeCOPY_DST = 0;
+                               curr_mode->ModeCOPY_AND = CHMOD_BITS;
+                               curr_mode->ModeOP = op;
                                break;
                        }
 
@@ -103,7 +120,7 @@ struct chmod_mode_struct *parse_chmod(const char *modestr,
                        modestr++;
 
                        state = STATE_1ST_HALF;
-                       where = what = op = topoct = topbits = flags = 0;
+                       where = what = op = topoct = topbits = flags = copybits = 0;
                }
 
                switch (state) {
@@ -159,26 +176,53 @@ struct chmod_mode_struct *parse_chmod(const char *modestr,
                case STATE_2ND_HALF:
                        switch (*modestr) {
                        case 'r':
+                               if (copybits)
+                                       state = STATE_ERROR;
                                what |= 4;
                                break;
                        case 'w':
+                               if (copybits)
+                                       state = STATE_ERROR;
                                what |= 2;
                                break;
                        case 'X':
+                               if (copybits)
+                                       state = STATE_ERROR;
                                flags |= FLAG_X_KEEP;
                                /* FALL THROUGH */
                        case 'x':
+                               if (copybits)
+                                       state = STATE_ERROR;
                                what |= 1;
                                break;
                        case 's':
+                               if (copybits)
+                                       state = STATE_ERROR;
                                if (topbits)
                                        topoct |= topbits;
                                else
                                        topoct = 04000;
                                break;
                        case 't':
+                               if (copybits)
+                                       state = STATE_ERROR;
                                topoct |= 01000;
                                break;
+                       case 'u':
+                               if (what || topoct || copybits)
+                                       state = STATE_ERROR;
+                               copybits = 0100;
+                               break;
+                       case 'g':
+                               if (what || topoct || copybits)
+                                       state = STATE_ERROR;
+                               copybits = 0010;
+                               break;
+                       case 'o':
+                               if (what || topoct || copybits)
+                                       state = STATE_ERROR;
+                               copybits = 0001;
+                               break;
                        default:
                                state = STATE_ERROR;
                                break;
@@ -212,6 +256,20 @@ struct chmod_mode_struct *parse_chmod(const char *modestr,
        return first_mode;
 }
 
+static int mode_copy_bits(int mode, int copy_src, int copy_dst, int copy_and)
+{
+       int copy_bits = 0;
+
+       if (copy_src & 0100)
+               copy_bits |= (mode >> 6) & 7;
+       if (copy_src & 0010)
+               copy_bits |= (mode >> 3) & 7;
+       if (copy_src & 0001)
+               copy_bits |= mode & 7;
+
+       return (copy_dst * copy_bits) & copy_and;
+}
+
 
 /* Takes an existing file permission and a list of AND/OR changes, and
  * create a new permissions. */
@@ -219,17 +277,25 @@ int tweak_mode(int mode, struct chmod_mode_struct *chmod_modes)
 {
        int IsX = mode & 0111;
        int NonPerm = mode & ~CHMOD_BITS;
+       int copy_bits;
 
        for ( ; chmod_modes; chmod_modes = chmod_modes->next) {
                if ((chmod_modes->flags & FLAG_DIRS_ONLY) && !S_ISDIR(NonPerm))
                        continue;
                if ((chmod_modes->flags & FLAG_FILES_ONLY) && S_ISDIR(NonPerm))
                        continue;
+               copy_bits = mode_copy_bits(mode, chmod_modes->ModeCOPY_SRC,
+                                          chmod_modes->ModeCOPY_DST,
+                                          chmod_modes->ModeCOPY_AND);
                mode &= chmod_modes->ModeAND;
                if ((chmod_modes->flags & FLAG_X_KEEP) && !IsX && !S_ISDIR(NonPerm))
                        mode |= chmod_modes->ModeOR & ~0111;
                else
                        mode |= chmod_modes->ModeOR;
+               if (chmod_modes->ModeOP == CHMOD_SUB)
+                       mode &= CHMOD_BITS - copy_bits;
+               else
+                       mode |= copy_bits;
        }
 
        return mode | NonPerm;
index 4aa7d652028c31ab1050c0f5064550aa04b765ed..1f0e79c110485fc7d9cc05f3e089fd7ac9956d15 100644 (file)
@@ -1523,6 +1523,12 @@ expand it.
 
     >     --chmod=D2775,F664
 
+    Symbolic permission-copy modes are also allowed, such as `g=u`, `o=g` or
+    `g-o`.  A permission-copy item may copy from one class only (`u`, `g` or
+    `o`) and cannot be combined with `rwxXst` permission letters in the same
+    item.  Use comma-separated items when you need both behaviours, such as
+    `g=o,o=`.
+
     It is also legal to specify multiple `--chmod` options, as each additional
     option is just appended to the list of changes to make.
 
index 57be6486da451df9df951b03ac726adde406022f..b1cf714e251533d2e3782e6ad4c8134b21c01811 100644 (file)
@@ -11,8 +11,8 @@ import shutil
 
 from rsyncfns import (
     FROMDIR, SCRATCHDIR, TODIR,
-    build_rsyncd_conf, checkit, makepath, rmtree,
-    run_rsync, start_test_daemon,
+    build_rsyncd_conf, check_perms, checkit, makepath, rmtree,
+    run_rsync, start_test_daemon, test_fail,
 )
 
 
@@ -62,6 +62,30 @@ for d in (checkdir, checkdir / 'dir1', checkdir / 'dir2'):
 checkit(['-avv', '--chmod', 'ug-s,a+rX,D+w', f'{FROMDIR}/', f'{TODIR}/'],
         checkdir, TODIR)
 
+def check_permcopy(chmod_arg, start_mode, expected):
+    rmtree(FROMDIR)
+    rmtree(TODIR)
+    makepath(FROMDIR)
+    (FROMDIR / 'permcopy').write_text('permcopy\n')
+    os.chmod(FROMDIR / 'permcopy', start_mode)
+    run_rsync('-avv', f'--chmod={chmod_arg}', f'{FROMDIR}/', f'{TODIR}/')
+    check_perms(TODIR / 'permcopy', expected)
+
+
+# Exercise chmod(1)-style permission copies.
+check_permcopy('g=o,o=', 0o647, 'rw-rwx---')
+check_permcopy('g=u', 0o741, 'rwxrwx--x')
+check_permcopy('g-o', 0o775, 'rwx-w-r-x')
+
+rmtree(FROMDIR)
+rmtree(TODIR)
+makepath(FROMDIR)
+(FROMDIR / 'permcopy').write_text('permcopy\n')
+proc = run_rsync('-avv', '--chmod=g=ur', f'{FROMDIR}/', f'{TODIR}/',
+                 check=False, capture_output=True)
+if proc.returncode == 0:
+    test_fail('--chmod=g=ur was not rejected')
+
 # Now exercise the F-only chmod path.
 rmtree(FROMDIR)
 rmtree(checkdir)