Commit | Line | Data |
---|---|---|
7170ca8d WD |
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 | ||
72e5645e | 12 | based-on: 3b8f8192227b14e708bf535072485e50f4362270 |
7170ca8d WD |
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 | @@ -81,6 +81,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; | |
72e5645e | 249 | @@ -283,7 +284,8 @@ static inline int path_is_daemon_excluded(char *path, int ignore_filename) |
7170ca8d WD |
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. */ | |
72e5645e | 259 | @@ -292,6 +294,8 @@ static int is_excluded(const char *fname, int is_dir, int filter_level) |
7170ca8d WD |
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 | |
72e5645e | 268 | @@ -1170,7 +1174,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist, |
7170ca8d WD |
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) | |
72e5645e | 277 | @@ -1215,6 +1219,12 @@ struct file_struct *make_file(const char *fname, struct file_list *flist, |
7170ca8d WD |
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) { | |
72e5645e | 290 | @@ -1415,12 +1425,23 @@ static struct file_struct *send_file_name(int f, struct file_list *flist, |
7170ca8d WD |
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) { | |
72e5645e | 315 | @@ -2280,7 +2301,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[]) |
7170ca8d WD |
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) { | |
72e5645e | 324 | @@ -2294,7 +2315,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[]) |
7170ca8d WD |
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 | gettimeofday(&end_tv, NULL); | |
333 | diff --git a/rsync.h b/rsync.h | |
334 | --- a/rsync.h | |
335 | +++ b/rsync.h | |
72e5645e | 336 | @@ -149,6 +149,9 @@ |
7170ca8d WD |
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) | |
72e5645e | 346 | @@ -805,6 +808,8 @@ struct map_struct { |
7170ca8d WD |
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 "**" */ | |
72e5645e | 355 | @@ -825,8 +830,18 @@ struct map_struct { |
7170ca8d WD |
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; | |
72e5645e | 374 | @@ -836,6 +851,11 @@ typedef struct filter_struct { |
7170ca8d WD |
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. | |
72e5645e | 398 | @@ -1831,6 +1833,10 @@ be omitted, but if USER is empty, a leading colon must be supplied. |
7170ca8d WD |
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. | |
72e5645e | 409 | @@ -2643,6 +2649,15 @@ itemization( |
7170ca8d WD |
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) | |
72e5645e | 425 | @@ -2704,6 +2719,12 @@ itemization( |
7170ca8d WD |
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 | |
72e5645e | 441 | @@ -816,6 +816,25 @@ size_t stringjoin(char *dest, size_t destsize, ...) |
7170ca8d WD |
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; |