Патчи для утилит find и cp во FreeBSD, реализующие полноценную работу с расширенными атрибутами

Дмитрий Филатов
Механизм расширенных атрибутов во FreeBSD (extattr) позволяет управлять дополнительной информацией, связанной с файлом или директорией. При помощи семейства команд setextattr, getextattr и rmextattr можно редактировать расширенные атрибуты файла, принадлежащие к двум пространствам имён: системному и пользовательскому. Редактирование пользовательских атрибутов может пригодиться для классификации файла: добавления описания, ключевых слов и других метаданных. Вот пример использования этих команд.

Установить атрибут: user - пользовательское пространство имён, keywords - название атрибута, далее значение и имя файла

$ setextattr user keywords forest,lake,stone 20190621.jpg

Прочесть значение атрибута

$ getextattr user keywords 20190621.jpg
20190621.jpg forest,lake,stone

Наиболее очевидный пример использования расширенных атрибутов - классификация файлов при помощи ключевых слов. Однако, механизм расширенных атрибутов во FreeBSD имеет ряд недоработок. В частности, расширенные атрибуты теряются при копировании файлов командой cp. Кроме того, утилита find не умеет осуществлять поиск файлов по их расширенным атрибутам. Без исправления этих недостатков теряет всякий смысл идея классификации и поиска файлов по ключевым словам или краткому описанию.

Для решения возникших препятствий я написал два патча - для утилит cp и find соответственно - исправляющих указанные недостатки. Подробнее с описанием патчей Вы можете ознакомиться в моих статьях на Хабре: "Как пропатчить find под FreeBSD?" и "Как пропатчить cp под FreeBSD?", здесь же я приведу окончательный результат своей работы.

Патч для утилиты find

--- /usr/src/usr.bin/find/extern.h.orig
+++ /usr/src/usr.bin/find/extern.h
@@ -76,6 +76,7 @@
creat_f c_sparse;
creat_f c_type;
creat_f c_user;
+creat_f c_userattr;
creat_f c_xdev;

exec_f f_Xmin;
@@ -113,6 +114,7 @@
exec_f f_sparse;
exec_f f_type;
exec_f f_user;
+exec_f f_userattr;

extern int ftsoptions, ignore_readdir_race, isdeprecated, isdepth, isoutput;
extern int issort, isxargs;
--- /usr/src/usr.bin/find/find.h.orig
+++ /usr/src/usr.bin/find/find.h
@@ -107,6 +107,7 @@
char *_a_data[2]; /* array of char pointers */
char *_c_data; /* char pointer */
regex_t *_re_data; /* regex */
+ char *_uattr_data; /* extattr (user namespace) */
} p_un;
} PLAN;
#define a_data p_un._a_data
@@ -134,6 +135,7 @@
#define e_pbsize p_un.ex._e_pbsize
#define e_psizemax p_un.ex._e_psizemax
#define e_next p_un.ex._e_next
+#define uattr_data p_un._uattr_data

typedef struct _option {
const char *name; /* option name */
--- /usr/src/usr.bin/find/function.c.orig
+++ /usr/src/usr.bin/find/function.c
@@ -46,6 +46,7 @@
#include <sys/acl.h>
#include <sys/wait.h>
#include <sys/mount.h>
+#include <sys/extattr.h>

#include <dirent.h>
#include <err.h>
@@ -1660,6 +1661,61 @@
}

/*
+ * -userattr
+ */
+
+int
+f_userattr(PLAN *plan, FTSENT *entry)
+{
+ regex_t re;
+ char *rest = NULL;
+ char *attr, *val = NULL;
+ char uattr[strlen(plan->uattr_data) + 1];
+ memset(uattr, '\0', strlen(plan->uattr_data) + 1);
+
+ strncpy(uattr, plan->uattr_data, strlen(plan->uattr_data));
+
+ attr = strtok_r(uattr, "=", &rest);
+ val = strtok_r(NULL, "=", &rest);
+
+ int match = 0;
+ char getval[1024] = {0};
+
+ if (S_ISLNK(entry->fts_statp->st_mode)) {
+ ssize_t b = extattr_get_link(entry->fts_accpath, EXTATTR_NAMESPACE_USER, attr, getval, 1024);
+ if(b > 0) {
+ int k = regcomp(&re, val, REG_EXTENDED|REG_ICASE);
+ if(k != 0) errx(1, "Syntax error in the regular expression");
+ int t = regexec(&re, getval, 0, NULL, 0);
+ match = (t == 0) ? 1 : 0;
+ }
+ } else {
+ ssize_t b = extattr_get_file(entry->fts_accpath, EXTATTR_NAMESPACE_USER, attr, getval, 1024);
+ if(b > 0) {
+ int k = regcomp(&re, val, REG_EXTENDED|REG_ICASE);
+ if(k != 0) errx(1, "Syntax error in the regular expression");
+ int t = regexec(&re, getval, 0, NULL, 0);
+ match = (t == 0) ? 1 : 0;
+ }
+ }
+ regfree(&re);
+
+ return match;
+}
+
+PLAN *
+c_userattr(OPTION *option, char ***argvp)
+{
+ char *pattern;
+ PLAN *new;
+ ftsoptions &= ~FTS_NOSTAT;
+ pattern = nextarg(option, argvp);
+ new = palloc(option);
+ new->uattr_data = pattern;
+ return new;
+}
+
+/*
* -xdev functions --
*
* Always true, causes find not to descend past directories that have a
--- /usr/src/usr.bin/find/option.c.orig
+++ /usr/src/usr.bin/find/option.c
@@ -150,6 +150,7 @@
{ "-type", c_type, f_type, 0 },
{ "-uid", c_user, f_user, 0 },
{ "-user", c_user, f_user, 0 },
+ { "-userattr", c_userattr, f_userattr, 0 },
{ "-wholename", c_name, f_path, 0 },
{ "-xdev", c_xdev, f_always_true, 0 },
// -xtype

Он же на Github Gist.

Применить его можно так (во FreeBSD должны быть установлены исходники):

cd /usr/src/usr.bin/find
patch < /tmp/patch-find.diff
make
make install clean

А использовать так:

find . -userattr keywords=lake
./20190621.jpg

Эта команда найдёт все файлы, у которых атрибут "keywords" содержит подстроку "lake".

Патч для утилиты cp

--- /usr/src/bin/cp/extern.h.orig
+++ /usr/src/bin/cp/extern.h
@@ -49,4 +49,5 @@
int preserve_dir_acls(struct stat *, char *, char *);
int preserve_fd_acls(int, int);
void usage(void);
+int user_extattr_copy_fd(int, int);
__END_DECLS
--- /usr/src/bin/cp/utils.c.orig
+++ /usr/src/bin/cp/utils.c
@@ -39,6 +39,7 @@
#include <sys/acl.h>
#include <sys/param.h>
#include <sys/stat.h>
+#include <sys/extattr.h>
#ifdef VM_AND_BUFFER_CACHE_SYNCHRONIZED
#include <sys/mman.h>
#endif
@@ -50,6 +51,7 @@
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
+#include <string.h>
#include <sysexits.h>
#include <unistd.h>

@@ -262,6 +264,8 @@
rval = 1;
if (pflag && preserve_fd_acls(from_fd, to_fd) != 0)
rval = 1;
+ if(user_extattr_copy_fd(from_fd, to_fd) != 0)
+ rval = 1;
if (close(to_fd)) {
warn("%s", to.p_path);
rval = 1;
@@ -557,3 +561,76 @@
"target_directory");
exit(EX_USAGE);
}
+
+int
+user_extattr_copy_fd(int from_fd, int to_fd)
+{
+ ssize_t llen, vlen, maxvlen;
+ size_t alen;
+ void *alist = NULL;
+ void *aval = NULL;
+ size_t i;
+ int error = -1;
+
+ llen = extattr_list_fd(from_fd, EXTATTR_NAMESPACE_USER, NULL, 0);
+ if (llen == -1) {
+ /* Silently ignore when EA are not supported */
+ if (errno == EOPNOTSUPP)
+ error = 0;
+ goto out;
+ }
+
+ if (llen == 0) {
+ error = 0;
+ goto out;
+ }
+
+ if ((alist = malloc((size_t)llen)) == NULL)
+ goto out;
+
+ llen = extattr_list_fd(from_fd, EXTATTR_NAMESPACE_USER, alist, (size_t)llen);
+ if (llen == -1)
+ goto out;
+
+ maxvlen = 1024;
+ if ((aval = malloc((size_t)maxvlen)) == NULL)
+ goto out;
+
+ for (i = 0; i < (size_t)llen; i += alen + 1) {
+ char aname[NAME_MAX + 1];
+ char *ap;
+
+ alen = ((uint8_t *)alist)[i];
+ ap = ((char *)alist) + i + 1;
+ (void)memcpy(aname, ap, alen);
+ aname[alen] = '\0';
+
+ vlen = extattr_get_fd(from_fd, EXTATTR_NAMESPACE_USER, aname, NULL, 0);
+ if (vlen == -1)
+ goto out;
+
+ if (vlen > maxvlen) {
+ if ((aval = realloc(aval, (size_t)vlen)) == NULL)
+ goto out;
+ maxvlen = vlen;
+ }
+
+ if ((vlen = extattr_get_fd(from_fd, EXTATTR_NAMESPACE_USER, aname,
+ aval, (size_t)vlen)) == -1)
+ goto out;
+
+ if (extattr_set_fd(to_fd, EXTATTR_NAMESPACE_USER, aname,
+ aval, (size_t)vlen) != vlen)
+ goto out;
+ }
+
+ error = 0;
+out:
+ if (aval != NULL)
+ free(aval);
+
+ if (alist != NULL)
+ free(alist);
+
+ return error;
+}

Или на Github Gist.

Применяется аналогичным образом:

cd /usr/src/bin/cp
patch < /tmp/patch-cp.diff
make
make install clean

После его применения расширенные атрибуты будут по-умолчанию копироваться вместе с файлом.

Полный код утилит с моими изменениями размещён в репозиториях find и cp.
2019-01-29