| 1 | From: Matt McCutchen <matt@mattmccutchen.net> |
| 2 | |
| 3 | Implement the "m", "o", "g" include modifiers to tweak the permissions, |
| 4 | owner, or group of matching files. |
| 5 | |
| 6 | To use this patch, run these commands for a successful build: |
| 7 | |
| 8 | patch -p1 <patches/filter-attribute-mods.diff |
| 9 | ./configure (optional if already run) |
| 10 | make |
| 11 | |
| 12 | based-on: a01e3b490eb36ccf9e704840e1b6683dab867550 |
| 13 | diff --git a/exclude.c b/exclude.c |
| 14 | --- a/exclude.c |
| 15 | +++ b/exclude.c |
| 16 | @@ -44,8 +44,11 @@ filter_rule_list filter_list = { .debug_type = "" }; |
| 17 | filter_rule_list cvs_filter_list = { .debug_type = " [global CVS]" }; |
| 18 | filter_rule_list daemon_filter_list = { .debug_type = " [daemon]" }; |
| 19 | |
| 20 | -/* Need room enough for ":MODS " prefix plus some room to grow. */ |
| 21 | -#define MAX_RULE_PREFIX (16) |
| 22 | +filter_rule *last_hit_filter_rule; |
| 23 | + |
| 24 | +/* Need room enough for ":MODS " prefix, which can now include |
| 25 | + * chmod/user/group values. */ |
| 26 | +#define MAX_RULE_PREFIX (256) |
| 27 | |
| 28 | #define SLASH_WILD3_SUFFIX "/***" |
| 29 | |
| 30 | @@ -118,8 +121,27 @@ static void teardown_mergelist(filter_rule *ex) |
| 31 | mergelist_cnt--; |
| 32 | } |
| 33 | |
| 34 | +static struct filter_chmod_struct *ref_filter_chmod(struct filter_chmod_struct *chmod) |
| 35 | +{ |
| 36 | + chmod->ref_cnt++; |
| 37 | + assert(chmod->ref_cnt != 0); /* Catch overflow. */ |
| 38 | + return chmod; |
| 39 | +} |
| 40 | + |
| 41 | +static void unref_filter_chmod(struct filter_chmod_struct *chmod) |
| 42 | +{ |
| 43 | + chmod->ref_cnt--; |
| 44 | + if (chmod->ref_cnt == 0) { |
| 45 | + free(chmod->modestr); |
| 46 | + free_chmod_mode(chmod->modes); |
| 47 | + free(chmod); |
| 48 | + } |
| 49 | +} |
| 50 | + |
| 51 | static void free_filter(filter_rule *ex) |
| 52 | { |
| 53 | + if (ex->rflags & FILTRULE_CHMOD) |
| 54 | + unref_filter_chmod(ex->chmod); |
| 55 | free(ex->pattern); |
| 56 | free(ex); |
| 57 | } |
| 58 | @@ -722,7 +744,8 @@ static void report_filter_result(enum logcode code, char const *name, |
| 59 | } |
| 60 | |
| 61 | /* Return -1 if file "name" is defined to be excluded by the specified |
| 62 | - * exclude list, 1 if it is included, and 0 if it was not matched. */ |
| 63 | + * exclude list, 1 if it is included, and 0 if it was not matched. |
| 64 | + * Sets last_hit_filter_rule to the filter that was hit, or NULL if none. */ |
| 65 | int check_filter(filter_rule_list *listp, enum logcode code, |
| 66 | const char *name, int name_is_dir) |
| 67 | { |
| 68 | @@ -748,10 +771,12 @@ int check_filter(filter_rule_list *listp, enum logcode code, |
| 69 | if (rule_matches(name, ent, name_is_dir)) { |
| 70 | report_filter_result(code, name, ent, name_is_dir, |
| 71 | listp->debug_type); |
| 72 | + last_hit_filter_rule = ent; |
| 73 | return ent->rflags & FILTRULE_INCLUDE ? 1 : -1; |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | + last_hit_filter_rule = NULL; |
| 78 | return 0; |
| 79 | } |
| 80 | |
| 81 | @@ -768,9 +793,46 @@ static const uchar *rule_strcmp(const uchar *str, const char *rule, int rule_len |
| 82 | return NULL; |
| 83 | } |
| 84 | |
| 85 | +static char *grab_paren_value(const uchar **s_ptr) |
| 86 | +{ |
| 87 | + const uchar *start, *end; |
| 88 | + int val_sz; |
| 89 | + char *val; |
| 90 | + |
| 91 | + if ((*s_ptr)[1] != '(') |
| 92 | + return NULL; |
| 93 | + start = (*s_ptr) + 2; |
| 94 | + |
| 95 | + for (end = start; *end != ')'; end++) |
| 96 | + if (!*end || *end == ' ' || *end == '_') |
| 97 | + return NULL; |
| 98 | + |
| 99 | + val_sz = end - start + 1; |
| 100 | + val = new_array(char, val_sz); |
| 101 | + strlcpy(val, (const char *)start, val_sz); |
| 102 | + *s_ptr = end; /* remember ++s in parse_rule_tok */ |
| 103 | + return val; |
| 104 | +} |
| 105 | + |
| 106 | +static struct filter_chmod_struct *make_chmod_struct(char *modestr) |
| 107 | +{ |
| 108 | + struct filter_chmod_struct *chmod; |
| 109 | + struct chmod_mode_struct *modes = NULL; |
| 110 | + |
| 111 | + if (!parse_chmod(modestr, &modes)) |
| 112 | + return NULL; |
| 113 | + |
| 114 | + if (!(chmod = new(struct filter_chmod_struct))) |
| 115 | + out_of_memory("make_chmod_struct"); |
| 116 | + chmod->ref_cnt = 1; |
| 117 | + chmod->modestr = modestr; |
| 118 | + chmod->modes = modes; |
| 119 | + return chmod; |
| 120 | +} |
| 121 | + |
| 122 | #define FILTRULES_FROM_CONTAINER (FILTRULE_ABS_PATH | FILTRULE_INCLUDE \ |
| 123 | | FILTRULE_DIRECTORY | FILTRULE_NEGATE \ |
| 124 | - | FILTRULE_PERISHABLE) |
| 125 | + | FILTRULE_PERISHABLE | FILTRULES_ATTRS) |
| 126 | |
| 127 | /* Gets the next include/exclude rule from *rulestr_ptr and advances |
| 128 | * *rulestr_ptr to point beyond it. Stores the pattern's start (within |
| 129 | @@ -785,6 +847,7 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr, |
| 130 | const char **pat_ptr, unsigned int *pat_len_ptr) |
| 131 | { |
| 132 | const uchar *s = (const uchar *)*rulestr_ptr; |
| 133 | + char *val; |
| 134 | filter_rule *rule; |
| 135 | unsigned int len; |
| 136 | |
| 137 | @@ -804,6 +867,12 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr, |
| 138 | /* Inherit from the template. Don't inherit FILTRULES_SIDES; we check |
| 139 | * that later. */ |
| 140 | rule->rflags = template->rflags & FILTRULES_FROM_CONTAINER; |
| 141 | + if (template->rflags & FILTRULE_CHMOD) |
| 142 | + rule->chmod = ref_filter_chmod(template->chmod); |
| 143 | + if (template->rflags & FILTRULE_FORCE_OWNER) |
| 144 | + rule->force_uid = template->force_uid; |
| 145 | + if (template->rflags & FILTRULE_FORCE_GROUP) |
| 146 | + rule->force_gid = template->force_gid; |
| 147 | |
| 148 | /* Figure out what kind of a filter rule "s" is pointing at. Note |
| 149 | * that if FILTRULE_NO_PREFIXES is set, the rule is either an include |
| 150 | @@ -949,11 +1018,63 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr, |
| 151 | goto invalid; |
| 152 | rule->rflags |= FILTRULE_EXCLUDE_SELF; |
| 153 | break; |
| 154 | + case 'g': { |
| 155 | + gid_t gid; |
| 156 | + |
| 157 | + if (!(val = grab_paren_value(&s))) |
| 158 | + goto invalid; |
| 159 | + if (group_to_gid(val, &gid, True)) { |
| 160 | + rule->rflags |= FILTRULE_FORCE_GROUP; |
| 161 | + rule->force_gid = gid; |
| 162 | + } else { |
| 163 | + rprintf(FERROR, |
| 164 | + "unknown group '%s' in filter rule: %s\n", |
| 165 | + val, *rulestr_ptr); |
| 166 | + exit_cleanup(RERR_SYNTAX); |
| 167 | + } |
| 168 | + free(val); |
| 169 | + break; |
| 170 | + } |
| 171 | + case 'm': { |
| 172 | + struct filter_chmod_struct *chmod; |
| 173 | + |
| 174 | + if (!(val = grab_paren_value(&s))) |
| 175 | + goto invalid; |
| 176 | + if ((chmod = make_chmod_struct(val))) { |
| 177 | + if (rule->rflags & FILTRULE_CHMOD) |
| 178 | + unref_filter_chmod(rule->chmod); |
| 179 | + rule->rflags |= FILTRULE_CHMOD; |
| 180 | + rule->chmod = chmod; |
| 181 | + } else { |
| 182 | + rprintf(FERROR, |
| 183 | + "unparseable chmod string '%s' in filter rule: %s\n", |
| 184 | + val, *rulestr_ptr); |
| 185 | + exit_cleanup(RERR_SYNTAX); |
| 186 | + } |
| 187 | + break; |
| 188 | + } |
| 189 | case 'n': |
| 190 | if (!(rule->rflags & FILTRULE_MERGE_FILE)) |
| 191 | goto invalid; |
| 192 | rule->rflags |= FILTRULE_NO_INHERIT; |
| 193 | break; |
| 194 | + case 'o': { |
| 195 | + uid_t uid; |
| 196 | + |
| 197 | + if (!(val = grab_paren_value(&s))) |
| 198 | + goto invalid; |
| 199 | + if (user_to_uid(val, &uid, True)) { |
| 200 | + rule->rflags |= FILTRULE_FORCE_OWNER; |
| 201 | + rule->force_uid = uid; |
| 202 | + } else { |
| 203 | + rprintf(FERROR, |
| 204 | + "unknown user '%s' in filter rule: %s\n", |
| 205 | + val, *rulestr_ptr); |
| 206 | + exit_cleanup(RERR_SYNTAX); |
| 207 | + } |
| 208 | + free(val); |
| 209 | + break; |
| 210 | + } |
| 211 | case 'p': |
| 212 | rule->rflags |= FILTRULE_PERISHABLE; |
| 213 | break; |
| 214 | @@ -1275,6 +1396,23 @@ char *get_rule_prefix(filter_rule *rule, const char *pat, int for_xfer, |
| 215 | else if (am_sender) |
| 216 | return NULL; |
| 217 | } |
| 218 | + if (rule->rflags & FILTRULES_ATTRS) { |
| 219 | + if (!for_xfer || protocol_version >= 31) { |
| 220 | + if (rule->rflags & FILTRULE_CHMOD) |
| 221 | + if (!snappendf(&op, (buf + sizeof buf) - op, |
| 222 | + "m(%s)", rule->chmod->modestr)) |
| 223 | + return NULL; |
| 224 | + if (rule->rflags & FILTRULE_FORCE_OWNER) |
| 225 | + if (!snappendf(&op, (buf + sizeof buf) - op, |
| 226 | + "o(%u)", (unsigned)rule->force_uid)) |
| 227 | + return NULL; |
| 228 | + if (rule->rflags & FILTRULE_FORCE_GROUP) |
| 229 | + if (!snappendf(&op, (buf + sizeof buf) - op, |
| 230 | + "g(%u)", (unsigned)rule->force_gid)) |
| 231 | + return NULL; |
| 232 | + } else if (!am_sender) |
| 233 | + return NULL; |
| 234 | + } |
| 235 | if (op - buf > legal_len) |
| 236 | return NULL; |
| 237 | if (legal_len) |
| 238 | diff --git a/flist.c b/flist.c |
| 239 | --- a/flist.c |
| 240 | +++ b/flist.c |
| 241 | @@ -82,6 +82,7 @@ extern struct chmod_mode_struct *chmod_modes; |
| 242 | |
| 243 | extern filter_rule_list filter_list; |
| 244 | extern filter_rule_list daemon_filter_list; |
| 245 | +extern filter_rule *last_hit_filter_rule; |
| 246 | |
| 247 | #ifdef ICONV_OPTION |
| 248 | extern int filesfrom_convert; |
| 249 | @@ -284,7 +285,8 @@ static inline int path_is_daemon_excluded(char *path, int ignore_filename) |
| 250 | |
| 251 | /* This function is used to check if a file should be included/excluded |
| 252 | * from the list of files based on its name and type etc. The value of |
| 253 | - * filter_level is set to either SERVER_FILTERS or ALL_FILTERS. */ |
| 254 | + * filter_level is set to either SERVER_FILTERS or ALL_FILTERS. |
| 255 | + * "last_hit_filter_rule" will be set to the operative filter, or NULL if none. */ |
| 256 | static int is_excluded(const char *fname, int is_dir, int filter_level) |
| 257 | { |
| 258 | #if 0 /* This currently never happens, so avoid a useless compare. */ |
| 259 | @@ -293,6 +295,8 @@ static int is_excluded(const char *fname, int is_dir, int filter_level) |
| 260 | #endif |
| 261 | if (is_daemon_excluded(fname, is_dir)) |
| 262 | return 1; |
| 263 | + /* Don't leave a daemon include in last_hit_filter_rule. */ |
| 264 | + last_hit_filter_rule = NULL; |
| 265 | if (filter_level != ALL_FILTERS) |
| 266 | return 0; |
| 267 | if (filter_list.head |
| 268 | @@ -1171,7 +1175,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist, |
| 269 | } else if (readlink_stat(thisname, &st, linkname) != 0) { |
| 270 | int save_errno = errno; |
| 271 | /* See if file is excluded before reporting an error. */ |
| 272 | - if (filter_level != NO_FILTERS |
| 273 | + if (filter_level != NO_FILTERS && filter_level != ALL_FILTERS_NO_EXCLUDE |
| 274 | && (is_excluded(thisname, 0, filter_level) |
| 275 | || is_excluded(thisname, 1, filter_level))) { |
| 276 | if (ignore_perishable && save_errno != ENOENT) |
| 277 | @@ -1216,6 +1220,12 @@ struct file_struct *make_file(const char *fname, struct file_list *flist, |
| 278 | |
| 279 | if (filter_level == NO_FILTERS) |
| 280 | goto skip_filters; |
| 281 | + if (filter_level == ALL_FILTERS_NO_EXCLUDE) { |
| 282 | + /* Call only for the side effect of setting last_hit_filter_rule to |
| 283 | + * any operative include filter, which might affect attributes. */ |
| 284 | + is_excluded(thisname, S_ISDIR(st.st_mode) != 0, ALL_FILTERS); |
| 285 | + goto skip_filters; |
| 286 | + } |
| 287 | |
| 288 | if (S_ISDIR(st.st_mode)) { |
| 289 | if (!xfer_dirs) { |
| 290 | @@ -1416,12 +1426,23 @@ static struct file_struct *send_file_name(int f, struct file_list *flist, |
| 291 | int flags, int filter_level) |
| 292 | { |
| 293 | struct file_struct *file; |
| 294 | + BOOL can_tweak_mode; |
| 295 | |
| 296 | file = make_file(fname, flist, stp, flags, filter_level); |
| 297 | if (!file) |
| 298 | return NULL; |
| 299 | |
| 300 | - if (chmod_modes && !S_ISLNK(file->mode) && file->mode) |
| 301 | + can_tweak_mode = !S_ISLNK(file->mode) && file->mode; |
| 302 | + if ((filter_level == ALL_FILTERS || filter_level == ALL_FILTERS_NO_EXCLUDE) |
| 303 | + && last_hit_filter_rule) { |
| 304 | + if ((last_hit_filter_rule->rflags & FILTRULE_CHMOD) && can_tweak_mode) |
| 305 | + file->mode = tweak_mode(file->mode, last_hit_filter_rule->chmod->modes); |
| 306 | + if ((last_hit_filter_rule->rflags & FILTRULE_FORCE_OWNER) && uid_ndx) |
| 307 | + F_OWNER(file) = last_hit_filter_rule->force_uid; |
| 308 | + if ((last_hit_filter_rule->rflags & FILTRULE_FORCE_GROUP) && gid_ndx) |
| 309 | + F_GROUP(file) = last_hit_filter_rule->force_gid; |
| 310 | + } |
| 311 | + if (chmod_modes && can_tweak_mode) |
| 312 | file->mode = tweak_mode(file->mode, chmod_modes); |
| 313 | |
| 314 | if (f >= 0) { |
| 315 | @@ -2300,7 +2321,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[]) |
| 316 | struct file_struct *file; |
| 317 | file = send_file_name(f, flist, fbuf, &st, |
| 318 | FLAG_TOP_DIR | FLAG_CONTENT_DIR | flags, |
| 319 | - NO_FILTERS); |
| 320 | + ALL_FILTERS_NO_EXCLUDE); |
| 321 | if (!file) |
| 322 | continue; |
| 323 | if (inc_recurse) { |
| 324 | @@ -2314,7 +2335,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[]) |
| 325 | } else |
| 326 | send_if_directory(f, flist, file, fbuf, len, flags); |
| 327 | } else |
| 328 | - send_file_name(f, flist, fbuf, &st, flags, NO_FILTERS); |
| 329 | + send_file_name(f, flist, fbuf, &st, flags, ALL_FILTERS_NO_EXCLUDE); |
| 330 | } |
| 331 | |
| 332 | if (reenable_multiplex >= 0) |
| 333 | diff --git a/rsync.h b/rsync.h |
| 334 | --- a/rsync.h |
| 335 | +++ b/rsync.h |
| 336 | @@ -151,6 +151,9 @@ |
| 337 | #define NO_FILTERS 0 |
| 338 | #define SERVER_FILTERS 1 |
| 339 | #define ALL_FILTERS 2 |
| 340 | +/* Don't let the file be excluded, but check for a filter that might affect |
| 341 | + * its attributes via FILTRULES_ATTRS. */ |
| 342 | +#define ALL_FILTERS_NO_EXCLUDE 3 |
| 343 | |
| 344 | #define XFLG_FATAL_ERRORS (1<<0) |
| 345 | #define XFLG_OLD_PREFIXES (1<<1) |
| 346 | @@ -809,6 +812,8 @@ struct map_struct { |
| 347 | int status; /* first errno from read errors */ |
| 348 | }; |
| 349 | |
| 350 | +struct chmod_mode_struct; |
| 351 | + |
| 352 | #define FILTRULE_WILD (1<<0) /* pattern has '*', '[', and/or '?' */ |
| 353 | #define FILTRULE_WILD2 (1<<1) /* pattern has '**' */ |
| 354 | #define FILTRULE_WILD2_PREFIX (1<<2) /* pattern starts with "**" */ |
| 355 | @@ -829,8 +834,18 @@ struct map_struct { |
| 356 | #define FILTRULE_RECEIVER_SIDE (1<<17)/* rule applies to the receiving side */ |
| 357 | #define FILTRULE_CLEAR_LIST (1<<18)/* this item is the "!" token */ |
| 358 | #define FILTRULE_PERISHABLE (1<<19)/* perishable if parent dir goes away */ |
| 359 | +#define FILTRULE_CHMOD (1<<20)/* chmod-tweak matching files */ |
| 360 | +#define FILTRULE_FORCE_OWNER (1<<21)/* force owner of matching files */ |
| 361 | +#define FILTRULE_FORCE_GROUP (1<<22)/* force group of matching files */ |
| 362 | |
| 363 | #define FILTRULES_SIDES (FILTRULE_SENDER_SIDE | FILTRULE_RECEIVER_SIDE) |
| 364 | +#define FILTRULES_ATTRS (FILTRULE_CHMOD | FILTRULE_FORCE_OWNER | FILTRULE_FORCE_GROUP) |
| 365 | + |
| 366 | +struct filter_chmod_struct { |
| 367 | + unsigned int ref_cnt; |
| 368 | + char *modestr; |
| 369 | + struct chmod_mode_struct *modes; |
| 370 | +}; |
| 371 | |
| 372 | typedef struct filter_struct { |
| 373 | struct filter_struct *next; |
| 374 | @@ -840,6 +855,11 @@ typedef struct filter_struct { |
| 375 | int slash_cnt; |
| 376 | struct filter_list_struct *mergelist; |
| 377 | } u; |
| 378 | + /* TODO: Use an "extras" mechanism to avoid |
| 379 | + * allocating this memory when we don't need it. */ |
| 380 | + struct filter_chmod_struct *chmod; |
| 381 | + uid_t force_uid; |
| 382 | + gid_t force_gid; |
| 383 | } filter_rule; |
| 384 | |
| 385 | typedef struct filter_list_struct { |
| 386 | diff --git a/rsync.yo b/rsync.yo |
| 387 | --- a/rsync.yo |
| 388 | +++ b/rsync.yo |
| 389 | @@ -1028,6 +1028,8 @@ quote(--chmod=Dg+s,ug+w,Fo-w,+X) |
| 390 | |
| 391 | It is also legal to specify multiple bf(--chmod) options, as each |
| 392 | additional option is just appended to the list of changes to make. |
| 393 | +To change permissions of files matching a pattern, use an include filter with |
| 394 | +the bf(m) modifier, which takes effect before any bf(--chmod) options. |
| 395 | |
| 396 | See the bf(--perms) and bf(--executability) options for how the resulting |
| 397 | permission value can be applied to the files in the transfer. |
| 398 | @@ -1846,6 +1848,10 @@ be omitted, but if USER is empty, a leading colon must be supplied. |
| 399 | If you specify "--chown=foo:bar, this is exactly the same as specifying |
| 400 | "--usermap=*:foo --groupmap=*:bar", only easier. |
| 401 | |
| 402 | +To change ownership of files matching a pattern, use an include filter with |
| 403 | +the bf(o) and bf(g) modifiers, which take effect before uid/gid mapping and |
| 404 | +therefore em(can) be mixed with bf(--usermap) and bf(--groupmap). |
| 405 | + |
| 406 | dit(bf(--timeout=TIMEOUT)) This option allows you to set a maximum I/O |
| 407 | timeout in seconds. If no data is transferred for the specified time |
| 408 | then rsync will exit. The default is 0, which means no timeout. |
| 409 | @@ -2671,6 +2677,15 @@ itemization( |
| 410 | option's default rules that exclude things like "CVS" and "*.o" are |
| 411 | marked as perishable, and will not prevent a directory that was removed |
| 412 | on the source from being deleted on the destination. |
| 413 | + it() An bf(m+nop()(CHMOD)) on an include rule tweaks the permissions of matching |
| 414 | + source files in the same way as bf(--chmod). This happens before any |
| 415 | + tweaks requested via bf(--chmod) options. |
| 416 | + it() An bf(o+nop()(USER)) on an include rule pretends that matching source files |
| 417 | + are owned by bf(USER) (a name or numeric uid). This happens before any uid |
| 418 | + mapping by name or bf(--usermap). |
| 419 | + it() A bf(g+nop()(GROUP)) on an include rule pretends that matching source files |
| 420 | + are owned by bf(GROUP) (a name or numeric gid). This happens before any gid |
| 421 | + mapping by name or bf(--groupmap). |
| 422 | ) |
| 423 | |
| 424 | manpagesection(MERGE-FILE FILTER RULES) |
| 425 | @@ -2732,6 +2747,12 @@ itemization( |
| 426 | a rule prefix such as bf(hide)). |
| 427 | ) |
| 428 | |
| 429 | +The attribute-affecting modifiers bf(m), bf(o), and bf(g) work only in client |
| 430 | +filters (not in daemon filters), and only the modifiers of the first matching |
| 431 | +rule are applied. As an example, assuming bf(--super) is enabled, the |
| 432 | +rule "+o+nop()(root)g+nop()(root)m+nop()(go=) *~" would ensure that all "backup" files belong to |
| 433 | +root and are not accessible to anyone else. |
| 434 | + |
| 435 | Per-directory rules are inherited in all subdirectories of the directory |
| 436 | where the merge-file was found unless the 'n' modifier was used. Each |
| 437 | subdirectory's rules are prefixed to the inherited per-directory rules |
| 438 | diff --git a/util.c b/util.c |
| 439 | --- a/util.c |
| 440 | +++ b/util.c |
| 441 | @@ -816,6 +816,25 @@ size_t stringjoin(char *dest, size_t destsize, ...) |
| 442 | return ret; |
| 443 | } |
| 444 | |
| 445 | +/* Append formatted text at *dest_ptr up to a maximum of sz (like snprintf). |
| 446 | + * On success, advance *dest_ptr and return True; on overflow, return False. */ |
| 447 | +BOOL snappendf(char **dest_ptr, size_t sz, const char *format, ...) |
| 448 | +{ |
| 449 | + va_list ap; |
| 450 | + size_t len; |
| 451 | + |
| 452 | + va_start(ap, format); |
| 453 | + len = vsnprintf(*dest_ptr, sz, format, ap); |
| 454 | + va_end(ap); |
| 455 | + |
| 456 | + if (len >= sz) |
| 457 | + return False; |
| 458 | + else { |
| 459 | + *dest_ptr += len; |
| 460 | + return True; |
| 461 | + } |
| 462 | +} |
| 463 | + |
| 464 | int count_dir_elements(const char *p) |
| 465 | { |
| 466 | int cnt = 0, new_component = 1; |