recyclix: dont recycle empty files by default
[hband-ld-preload-libs.git] / src / recyclix.c
blobee1bc8544034ad9b3b3db69737475e23783fa1a9
2 #include <sys/stat.h>
3 #include <bits/posix1_lim.h>
4 #include <fcntl.h>
5 #include <stdbool.h>
6 #include <stdlib.h>
7 #include <stdio.h>
8 #include <dlfcn.h>
9 #include <unistd.h>
10 #include <string.h>
11 #include <err.h>
12 #include <errno.h>
13 #include <libgen.h>
14 #include <regex.h>
16 #define REGEXP_SZLIMIT "^([0-9]+)([BkMG]?)(-([0-9]+)([BkMG]?))?$"
17 #define NMATCH_SZLIMIT 6
18 #define REGEXP_EXCLUDE "^!(.*)$"
19 #define NMATCH_EXCLUDE 2
21 off_t recyclix_tosize(const char * str, regoff_t num_start, int digits, regoff_t unit);
22 int recyclix_clonepath_recursive(char * p_path, char * p_donor);
25 int unlinkat(int dirfd, const char *pathname, int flags)
27 int (*real_unlinkat)(int dirfd, const char *pathname, int flags) = dlsym(RTLD_NEXT, "unlinkat");
28 int retval;
30 char *recyclers;
31 char *ptr, *tmp, *comma;
32 char recycler[_POSIX_PATH_MAX];
33 char recycled[_POSIX_PATH_MAX];
34 char atdir[_POSIX_PATH_MAX];
35 char fqpath[_POSIX_PATH_MAX];
36 char mp_path[_POSIX_PATH_MAX];
37 struct stat st1;
38 struct stat st2;
39 ino_t recycler_inode;
40 char *txt_unset = "unset RECYCLER environment if you do not care about Recycle bin";
41 off_t sz_min, sz_max;
42 regex_t regex;
43 regmatch_t pmatch[NMATCH_SZLIMIT];
44 int regex_err;
45 bool excluded;
48 recyclers = getenv("RECYCLER");
49 if(recyclers != NULL)
51 retval = fstatat(dirfd, pathname, &st1, (flags & ~AT_REMOVEDIR /* fstatat hates AT_REMOVEDIR */) | AT_SYMLINK_NOFOLLOW /* we are about to move/unlink the symlink itself */);
52 if(retval != 0)
54 warn("%s", pathname);
56 else
58 while(1)
60 ptr = strchrnul(recyclers, ':');
61 strncpy(recycler, recyclers, ptr - recyclers);
62 recycler[ptr - recyclers] = '\0';
64 if(strncmp(recycler, "~/", 2) == 0)
66 tmp = getenv("HOME");
67 if(tmp == NULL)
69 warnx("Could not resolve: %s", recycler);
70 recycler[0] = '\0';
72 else
74 snprintf(recycler, strlen(tmp) + 1 /* slash */ + (ptr - recyclers - 2 /* tilde+slash*/) + 1 /* 0x00 */, "%s/%s", tmp, recyclers + 2);
78 if(strlen(recycler) > 0)
80 /* Found a Recycle Bin */
81 #ifdef DEBUG
82 warnx("RECYCLER %s", recycler);
83 #endif
85 sz_min = 1; // ignore empty files by default
86 sz_max = 0; // no upper size limit by default
87 excluded = false;
89 /* Reading options for this Recycle Bin */
90 comma = strchr(recycler, ',');
91 if(comma != NULL)
93 comma[0] = '\0';
94 comma++;
96 while(comma != NULL && strlen(comma) > 0)
98 /* comma points to the rest of options */
99 tmp = strchr(comma, ',');
100 if(tmp != NULL)
102 tmp[0] = '\0';
103 /* comma now points to a single option */
104 tmp++;
105 /* tmp now points to the next option(s) */
109 regex_err = regcomp(&regex, REGEXP_SZLIMIT, REG_EXTENDED | REG_ICASE);
110 if(regex_err != 0)
112 goto regexp_error;
114 else if(regexec(&regex, comma, NMATCH_SZLIMIT, pmatch, 0) == 0)
116 #ifdef DEBUG
117 int i;
118 char *s;
119 char *e;
120 for(i=0; i<NMATCH_SZLIMIT; i++)
122 if(pmatch[i].rm_so == -1) break;
123 s = comma + pmatch[i].rm_so;
124 e = comma + pmatch[i].rm_eo;
125 if(i == 0) fprintf(stderr, "$& eq ");
126 else fprintf(stderr, "$%d eq ", i);
127 fprintf(stderr, "'%.*s' [%p:%p]\n", (e - s), s, s, e);
129 #endif
130 sz_min = recyclix_tosize(comma, pmatch[1].rm_so, pmatch[1].rm_eo - pmatch[1].rm_so, pmatch[2].rm_so);
131 sz_max = recyclix_tosize(comma, pmatch[4].rm_so, pmatch[4].rm_eo - pmatch[4].rm_so, pmatch[5].rm_so);
132 #ifdef DEBUG
133 warnx("size min %lu, size max %lu.", sz_min, sz_max);
134 #endif
135 if(sz_max != 0 && sz_min > sz_max)
137 warnx("recyclix: Insane size limits: %lu-%lu", sz_min, sz_max);
138 errno = ECANCELED;
139 return -1;
141 goto next_option;
144 regex_err = regcomp(&regex, REGEXP_EXCLUDE, REG_EXTENDED | REG_ICASE);
145 if(regex_err != 0)
147 goto regexp_error;
149 else if(regexec(&regex, comma, NMATCH_EXCLUDE, pmatch, 0) == 0)
151 strncpy(mp_path, comma + pmatch[1].rm_so, pmatch[1].rm_eo - pmatch[1].rm_so + 1); /* mp_path is carrying user defined regex pattern */
152 regex_err = regcomp(&regex, mp_path, REG_EXTENDED);
153 if(regex_err != 0)
155 goto regexp_error;
157 else
159 regex_err = regexec(&regex, pathname, 0, pmatch, 0);
160 #ifdef DEBUG
161 warnx("exclude: '%s' %c~ /%s/", pathname, regex_err == 0 ? '=' : '!', comma+1);
162 #endif
163 excluded = regex_err == 0 ? true : false;
165 goto next_option;
168 regexp_error:
169 regerror(regex_err, &regex, mp_path, sizeof(mp_path)); /* mp_path is carrying the regex error message */
170 warnx("recyclix: Unrecognized option: '%s' (%s)", comma, mp_path);
171 errno = ECANCELED;
172 return -1;
174 next_option:
175 /* Read the next option (or end) */
176 comma = tmp;
179 retval = stat(recycler, &st2);
180 recycler_inode = st2.st_ino;
181 if(retval != 0)
183 warn("stat: %s", recycler);
185 else
187 if(st2.st_dev != st1.st_dev)
189 /* Cross-device condition */
190 #ifdef DEBUG
191 warnx("xdev: %03u,%03u (looking for %03u,%03u)", major(st2.st_dev), minor(st2.st_dev), major(st1.st_dev), minor(st1.st_dev));
192 #endif
194 else if(st1.st_size < sz_min || (sz_max != 0 && st1.st_size > sz_max))
196 /* File size exceeds limit */
197 #ifdef DEBUG
198 warnx("filesize %lu, min %lu, max %lu (0=infinite)", st1.st_size, sz_min, sz_max);
199 #endif
201 else if(excluded)
203 /* pathname matched to exclude criteria */
204 #ifdef DEBUG
205 warnx("filename matched to exclude criteria");
206 #endif
208 else
210 /* Recycle Bin and the subject file are on the same fs */
211 /* ptr no longer points to the next RECYCLER */
213 if(pathname[0] == '/')
215 /* Absolute pathname given */
216 strcpy(fqpath, pathname);
218 else
220 /* Relative pathname given */
221 if(dirfd == AT_FDCWD)
223 if(getcwd(atdir, _POSIX_PATH_MAX) == NULL)
225 warn("getcwd");
226 retval = -1;
227 break;
230 else
232 sprintf(fqpath, "/proc/self/fd/%d", dirfd);
233 retval = readlink(fqpath, atdir, _POSIX_PATH_MAX);
234 if(retval == -1)
236 warn("readlink: %s", fqpath);
237 break;
239 atdir[retval] = '\0';
242 sprintf(fqpath, "%s/%s", atdir, pathname);
245 strcpy(atdir, fqpath);
246 tmp = dirname(atdir); /* atdir got changed */
247 ptr = realpath(tmp, NULL);
248 if(ptr == NULL)
250 warn("realpath: %s", tmp);
251 free(ptr);
252 break;
254 /* ptr now points to the resolved path name of fqpath's parent directory */
255 strcpy(atdir, fqpath);
256 tmp = (char*)basename(atdir); /* atdir got changed */
257 sprintf(fqpath, "%s/%s", ptr, tmp);
258 free(ptr);
260 retval = 0;
261 strcpy(mp_path, fqpath);
262 while(1)
264 /* Looking for filesystem's root */
265 strcpy(atdir, mp_path);
266 tmp = dirname(atdir); /* dirname will change atdir */
267 if(strcmp(tmp, "//") == 0)
269 tmp[1] = '\0';
271 retval = stat(tmp, &st2);
272 if(retval != 0)
274 warn("stat: %s", tmp);
275 break;
278 if(st2.st_ino == recycler_inode)
280 /* removing from under the Recycler really removes the file */
281 goto real_unlink;
282 break;
285 if(strcmp(mp_path, "/") == 0 && strcmp(tmp, "/") == 0)
287 /* fqpath is on the root fs */
288 break;
290 if(st1.st_dev != st2.st_dev)
292 /* mp_path is the mountpoint */
293 break;
295 /* Check the parent directory */
296 strcpy(mp_path, tmp);
298 if(retval != 0) break;
300 sprintf(recycled, "%s/%s", recycler, fqpath + strlen(mp_path));
301 #ifdef DEBUG
302 warnx("Filesystem root: %s", mp_path);
303 warnx("Recycled file: %s", recycled);
304 #endif
306 /* Copy directory path onto Recycler */
307 strcpy(atdir, recycled);
308 if(S_ISDIR(st1.st_mode))
310 /* Clone the file (actually the directory) to be removed itself */
311 tmp = atdir;
312 ptr = fqpath;
314 else
316 /* Clone the file's parent directory */
317 tmp = dirname(atdir);
318 ptr = dirname(fqpath);
320 retval = recyclix_clonepath_recursive(tmp, ptr);
321 if(retval != 0)
323 warn("mkdir: %s", tmp);
324 break;
327 if(S_ISDIR(st1.st_mode))
329 goto real_unlink;
331 else
333 retval = renameat(dirfd, pathname, 0, recycled);
334 if(retval != 0)
336 warn("renameat");
337 warnx("Failed to drop file to the Recycler, %s.", txt_unset);
341 /* ptr is unusable to continue */
342 break;
344 /* Looking for next Recycler ... */
348 if(ptr[0] == '\0')
350 #ifdef DEBUG
351 warnx("There is no valid Recycler, removing file.");
352 #endif
353 goto real_unlink;
354 break;
356 else
358 recyclers = ptr+1;
363 else
365 real_unlink:
366 retval = real_unlinkat(dirfd, pathname, flags);
369 return retval;
372 int recyclix_clonepath_recursive(char * p_path, char * p_donor)
374 int (*real_unlink)(const char *pathname) = dlsym(RTLD_NEXT, "unlink");
375 int retval;
376 char path[_POSIX_PATH_MAX];
377 char donor[_POSIX_PATH_MAX];
378 char * parent;
379 char * dparent;
380 struct stat st;
381 struct stat donor_st;
383 #ifdef DEBUG
384 warnx("%s: %s", __func__, p_path);
385 #endif
387 /* save stats of directory being cloned */
388 retval = stat(p_donor, &donor_st);
389 if(retval == -1)
391 warn("stat: %s", p_donor);
393 else
395 try_mkdir:
396 retval = mkdir(p_path, donor_st.st_mode);
397 if(retval == -1)
399 if(errno == ENOENT || errno == ENOTDIR)
401 strcpy(path, p_path);
402 strcpy(donor, p_donor);
403 parent = dirname(path);
404 dparent = dirname(donor);
406 retval = real_unlink(parent);
407 if(retval == 0)
409 goto try_mkdir;
411 else if(errno == ENOENT)
413 retval = recyclix_clonepath_recursive(parent, dparent);
414 if(retval == 0)
416 goto try_mkdir;
419 else
421 warn("unlink: %s", parent);
424 else if(errno == EEXIST)
426 retval = stat(p_path, &st);
427 if(retval == -1)
429 warn("stat: %s", p_path);
431 else
433 if(S_ISDIR(st.st_mode))
435 chmod(p_path, donor_st.st_mode);
436 retval = 0;
438 else
440 retval = real_unlink(p_path);
441 if(retval == 0)
443 goto try_mkdir;
445 else
447 warn("unlink: %s", p_path);
452 else
454 warn("mkdir: %s", p_path);
458 if(retval == 0)
460 chown(p_path, donor_st.st_uid, donor_st.st_gid);
462 return retval;
465 off_t recyclix_tosize(const char * str, regoff_t num_start, int digits, regoff_t unit)
467 char number[16];
468 off_t size;
470 if(num_start == -1)
472 return 0;
474 else
476 snprintf(number, 16>digits+1 ? digits+1 : 16, "%s", str+num_start);
477 size = atoi(number);
479 if(str[unit] == 'k' || str[unit] == 'K') size *= 1024;
480 else if(str[unit] == 'm' || str[unit] == 'M') size *= 1024*1024;
481 else if(str[unit] == 'g' || str[unit] == 'G') size *= 1024*1024*1024;
482 return size;
485 int unlink(const char *pathname)
487 return unlinkat(AT_FDCWD, pathname, 0);