]> git.hungrycats.org Git - linux/commitdiff
btrfs: props: add per-inode compression level support
authorZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Fri, 7 Aug 2026 01:43:39 +0000 (21:43 -0400)
committerZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Wed, 16 Sep 2026 21:39:58 +0000 (17:39 -0400)
Setting a per-file compression level is an often requested feature, and
the btrfs.compression property has silently accepted level suffixes
("zstd:9") since compression types were added to it:  the property
validator only matches the algorithm name prefix, the verbatim string is
stored in the xattr and returned by getxattr, and everything after the
algorithm name is ignored when the value is parsed into the in-memory
compression type.

All of the pieces needed to honor the level already exist:  the level is
a per-call argument down the whole compression path, per-inode levels
are already implemented for the defrag ioctl (defrag_compress_level),
and btrfs_compress_str2level() already parses and clamps ":level"
suffixes for the mount options.  Wire the property path up to them:

 * Cache the parsed level in a new btrfs_inode::prop_compress_level,
   with 0 meaning no level was specified, in which case the level from
   the mount options is used as before.  The field is signed to allow
   negative (realtime) zstd levels.

 * Parse an optional ":level" suffix in prop_compression_apply().
   Values stored by old kernels were never validated, so an unparseable
   suffix falls back to the default level rather than making the inode's
   properties fail to load.  Levels for lzo parse and clamp to nothing,
   matching commit 6db1df415d73 ("btrfs: accept and ignore compression
   level for lzo").

 * Use the level in compress_file_range() when compression is selected
   by the property.  The defrag ioctl retains precedence.

 * Regenerate the canonical "type:level" string with a new helper,
   btrfs_prop_compression_extract(), so that directory inheritance
   propagates the level to new inodes, and so that FS_IOC_SETFLAGS,
   which rewrites the property when setting FS_COMPR_FL, preserves the
   level instead of truncating the value to the bare algorithm name.
   The prop_handler extract hook itself is unchanged: it still returns
   a static string, and the inheritance loop regenerates the leveled
   value only for the compression property.

There is no disk format change:  the level lives in the already-existing
xattr value string.  Note that levels stored by old kernels (which were
accepted but ignored) become effective after this change.

Signed-off-by: Zygo Blaxell <ce3g8jdj@umail.furryterror.org>
Assisted-by: Claude:claude-fable-5
fs/btrfs/btrfs_inode.h
fs/btrfs/inode.c
fs/btrfs/ioctl.c
fs/btrfs/props.c
fs/btrfs/props.h

index b6b46f951f11a2cf373a932c1c4f9d1cf2bce4d8..6db2e853f3db7e6357a6b46b771d2fe05c757543 100644 (file)
@@ -140,6 +140,13 @@ struct btrfs_inode {
        /* Cached value of inode property 'compression'. */
        u8 prop_compress;
 
+       /*
+        * Cached compression level from the 'compression' property.  0 means
+        * no level was specified, in which case the mount option level is
+        * used.
+        */
+       s8 prop_compress_level;
+
        /*
         * Force compression on the file using the defrag ioctl, could be
         * different from prop_compress and takes precedence if set.
index cca9840bee8966e9c4a0bea9dbf4b98001dbfe3f..e02b4b6ea9beb791c5b5457c3b199bcb421ae798 100644 (file)
@@ -969,6 +969,8 @@ again:
                compress_level = inode->defrag_compress_level;
        } else if (inode->prop_compress) {
                compress_type = inode->prop_compress;
+               if (inode->prop_compress_level)
+                       compress_level = inode->prop_compress_level;
        }
 
        /* Compression level is applied here. */
index 2cd89e88332c164531495c043db6b3f8ee5129ef..4161012fd2b4001cd48f0065fe73de83e8637dcc 100644 (file)
@@ -255,6 +255,7 @@ int btrfs_fileattr_set(struct mnt_idmap *idmap,
        int ret;
        const char *comp = NULL;
        u32 old_inode_flags;
+       char comp_buf[16];
        u32 inode_flags;
        bool prop_set = false;
 
@@ -382,8 +383,10 @@ int btrfs_fileattr_set(struct mnt_idmap *idmap,
                inode_flags |= BTRFS_INODE_COMPRESS;
                inode_flags &= ~BTRFS_INODE_NOCOMPRESS;
 
-               comp = btrfs_compress_type2str(inode->prop_compress);
-               if (!comp || comp[0] == 0)
+               if (btrfs_prop_compression_extract(inode, comp_buf,
+                                                  sizeof(comp_buf)) > 0)
+                       comp = comp_buf;
+               else
                        comp = btrfs_compress_type2str(fs_info->compress_type);
                if (!comp || comp[0] == 0)
                        comp = btrfs_compress_type2str(BTRFS_COMPRESS_ZLIB);
index 8d25e517fa76a503a9f7d89b30ae4fc70dcdb345..4e3588580b8f99d92505c34a26fdb95ccdf6bc00 100644 (file)
@@ -307,11 +307,15 @@ static int prop_compression_apply(struct btrfs_inode *inode, const char *value,
        struct btrfs_fs_info *fs_info = inode->root->fs_info;
        int type;
 
+       size_t type_len;
+       int level = 0;
+
        /* Reset to defaults */
        if (len == 0) {
                inode->flags &= ~BTRFS_INODE_COMPRESS;
                inode->flags &= ~BTRFS_INODE_NOCOMPRESS;
                inode->prop_compress = BTRFS_COMPRESS_NONE;
+               inode->prop_compress_level = 0;
                return 0;
        }
 
@@ -321,29 +325,86 @@ static int prop_compression_apply(struct btrfs_inode *inode, const char *value,
                inode->flags |= BTRFS_INODE_NOCOMPRESS;
                inode->flags &= ~BTRFS_INODE_COMPRESS;
                inode->prop_compress = BTRFS_COMPRESS_NONE;
+               inode->prop_compress_level = 0;
 
                return 0;
        }
 
        if (!strncmp("lzo", value, 3)) {
                type = BTRFS_COMPRESS_LZO;
+               type_len = 3;
                btrfs_set_fs_incompat(fs_info, COMPRESS_LZO);
        } else if (!strncmp("zlib", value, 4)) {
                type = BTRFS_COMPRESS_ZLIB;
+               type_len = 4;
        } else if (!strncmp("zstd", value, 4)) {
                type = BTRFS_COMPRESS_ZSTD;
+               type_len = 4;
                btrfs_set_fs_incompat(fs_info, COMPRESS_ZSTD);
        } else {
                return -EINVAL;
        }
 
+       /*
+        * Parse an optional ":level" suffix.  Old kernels accepted and
+        * silently ignored arbitrary suffixes after the algorithm name, and
+        * such values may still exist on disk, so fall back to the default
+        * level instead of returning an error when the suffix cannot be
+        * parsed, otherwise the inode's properties could not be loaded.
+        */
+       if (len > type_len && value[type_len] == ':') {
+               char str[16];
+               size_t suffix_len = len - type_len;
+
+               if (suffix_len < sizeof(str)) {
+                       memcpy(str, value + type_len, suffix_len);
+                       str[suffix_len] = 0;
+                       if (btrfs_compress_str2level(type, str, &level) < 0)
+                               level = 0;
+               }
+       }
+
        inode->flags &= ~BTRFS_INODE_NOCOMPRESS;
        inode->flags |= BTRFS_INODE_COMPRESS;
        inode->prop_compress = type;
+       inode->prop_compress_level = level;
 
        return 0;
 }
 
+/*
+ * Regenerate the canonical "type" or "type:level" value of the
+ * compression property from the inode's cached fields.  Used where the
+ * property is rewritten rather than parsed: FS_IOC_SETFLAGS preserving
+ * an existing property, and directory inheritance.
+ */
+ssize_t btrfs_prop_compression_extract(const struct btrfs_inode *inode,
+                                      char *buf, size_t size)
+{
+       int len;
+
+       switch (inode->prop_compress) {
+       case BTRFS_COMPRESS_ZLIB:
+       case BTRFS_COMPRESS_LZO:
+       case BTRFS_COMPRESS_ZSTD:
+               break;
+       default:
+               return -ENODATA;
+       }
+
+       if (inode->prop_compress_level)
+               len = snprintf(buf, size, "%s:%d",
+                              btrfs_compress_type2str(inode->prop_compress),
+                              inode->prop_compress_level);
+       else
+               len = snprintf(buf, size, "%s",
+                              btrfs_compress_type2str(inode->prop_compress));
+       if (len >= size)
+               return -EOVERFLOW;
+
+       return len;
+}
+
 static bool prop_compression_ignore(const struct btrfs_inode *inode)
 {
        /*
@@ -401,6 +462,7 @@ int btrfs_inode_inherit_props(struct btrfs_trans_handle *trans,
        for (i = 0; i < ARRAY_SIZE(prop_handlers); i++) {
                const struct prop_handler *h = &prop_handlers[i];
                const char *value;
+               char buf[16];
                u64 num_bytes = 0;
 
                if (!h->inheritable)
@@ -413,6 +475,15 @@ int btrfs_inode_inherit_props(struct btrfs_trans_handle *trans,
                if (!value)
                        continue;
 
+               /*
+                * The extract hooks return static strings, which cannot
+                * carry the compression property's optional ":level"
+                * suffix; regenerate the full value for inheritance.
+                */
+               if (h->extract == prop_compression_extract &&
+                   btrfs_prop_compression_extract(parent, buf, sizeof(buf)) > 0)
+                       value = buf;
+
                /*
                 * This is not strictly necessary as the property should be
                 * valid, but in case it isn't, don't propagate it further.
index 15d9a025c923db65193d5f6266d205e744adceed..86a004be4e4690d52b42de9f2d30e445bd5090e3 100644 (file)
@@ -24,6 +24,9 @@ bool btrfs_ignore_prop(const struct btrfs_inode *inode, const char *name);
 
 int btrfs_load_inode_props(struct btrfs_inode *inode, struct btrfs_path *path);
 
+ssize_t btrfs_prop_compression_extract(const struct btrfs_inode *inode,
+                                      char *buf, size_t size);
+
 int btrfs_inode_inherit_props(struct btrfs_trans_handle *trans,
                              struct btrfs_inode *inode,
                              const struct btrfs_inode *dir);