1 /* Per-directory exclusion files for tar.
2
3 Copyright 2014-2023 Free Software Foundation, Inc.
4
5 This file is part of GNU tar.
6
7 GNU tar is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation; either version 3 of the License, or
10 (at your option) any later version.
11
12 GNU tar is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
16
17 You should have received a copy of the GNU General Public License
18 along with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20 #include <system.h>
21 #include <quotearg.h>
22 #include <flexmember.h>
23 #include <fnmatch.h>
24 #include <wordsplit.h>
25 #include "common.h"
26
27 typedef void (*add_fn) (struct exclude *, char const *, int, void *);
28
29 struct vcs_ignore_file
30 {
31 char const *filename;
32 int flags;
33 add_fn addfn;
34 void *(*initfn) (void *);
35 void *data;
36 };
37
38 static struct vcs_ignore_file *get_vcs_ignore_file (const char *name);
39
40 struct excfile
41 {
42 struct excfile *next;
43 int flags;
44 char name[FLEXIBLE_ARRAY_MEMBER];
45 };
46
47 static struct excfile *excfile_head, *excfile_tail;
48
49 void
50 excfile_add (const char *name, int flags)
51 {
52 struct excfile *p = xmalloc (FLEXNSIZEOF (struct excfile, name,
53 strlen (name) + 1));
54 p->next = NULL;
55 p->flags = flags;
56 strcpy (p->name, name);
57 if (excfile_tail)
58 excfile_tail->next = p;
59 else
60 excfile_head = p;
61 excfile_tail = p;
62 }
63
64 struct exclist
65 {
66 struct exclist *next, *prev;
67 int flags;
68 struct exclude *excluded;
69 };
70
71 void
72 info_attach_exclist (struct tar_stat_info *dir)
73 {
74 struct excfile *file;
75 struct exclist *head = NULL, *tail = NULL, *ent;
76 struct vcs_ignore_file *vcsfile;
77
78 if (dir->exclude_list)
79 return;
80 for (file = excfile_head; file; file = file->next)
81 {
82 if (faccessat (dir ? dir->fd : chdir_fd, file->name, F_OK, 0) == 0)
83 {
84 FILE *fp;
85 struct exclude *ex = NULL;
86 int fd = subfile_open (dir, file->name, O_RDONLY);
87 if (fd == -1)
88 {
89 open_error (file->name);
90 continue;
91 }
92 fp = fdopen (fd, "r");
93 if (!fp)
94 {
95 ERROR ((0, errno, _("%s: fdopen failed"), file->name));
96 close (fd);
97 continue;
98 }
99
100 if (!ex)
101 ex = new_exclude ();
102
103 vcsfile = get_vcs_ignore_file (file->name);
104
105 if (vcsfile->initfn)
106 vcsfile->data = vcsfile->initfn (vcsfile->data);
107
108 if (add_exclude_fp (vcsfile->addfn, ex, fp,
109 FNM_FILE_NAME|EXCLUDE_WILDCARDS|EXCLUDE_ANCHORED,
110 '\n',
111 vcsfile->data))
112 {
113 int e = errno;
114 FATAL_ERROR ((0, e, "%s", quotearg_colon (file->name)));
115 }
116 fclose (fp);
117
118 ent = xmalloc (sizeof (*ent));
119 ent->excluded = ex;
120 ent->flags = file->flags == EXCL_DEFAULT
121 ? file->flags : vcsfile->flags;
122 ent->prev = tail;
123 ent->next = NULL;
124
125 if (tail)
126 tail->next = ent;
127 else
128 head = ent;
129 tail = ent;
130 }
131 }
132 dir->exclude_list = head;
133 }
134
135 void
136 info_free_exclist (struct tar_stat_info *dir)
137 {
138 struct exclist *ep = dir->exclude_list;
139
140 while (ep)
141 {
142 struct exclist *next = ep->next;
143 free_exclude (ep->excluded);
144 free (ep);
145 ep = next;
146 }
147
148 dir->exclude_list = NULL;
149 }
150
151
152 /* Return nonzero if file NAME is excluded. */
153 bool
154 excluded_name (char const *name, struct tar_stat_info *st)
155 {
156 struct exclist *ep;
157 const char *rname = NULL;
158 char *bname = NULL;
159 bool result;
160 int nr = 0;
161
162 name += FILE_SYSTEM_PREFIX_LEN (name);
163
164 /* Try global exclusion list first */
165 if (excluded_file_name (excluded, name))
166 return true;
167
168 if (!st)
169 return false;
170
171 for (result = false; st && !result; st = st->parent, nr = EXCL_NON_RECURSIVE)
172 {
173 for (ep = st->exclude_list; ep; ep = ep->next)
174 {
175 if (ep->flags & nr)
176 continue;
177 if ((result = excluded_file_name (ep->excluded, name)))
178 break;
179
180 if (!rname)
181 {
182 rname = name;
183 /* Skip leading ./ */
184 while (*rname == '.' && ISSLASH (rname[1]))
185 rname += 2;
186 }
187 if ((result = excluded_file_name (ep->excluded, rname)))
188 break;
189
190 if (!bname)
191 bname = base_name (name);
192 if ((result = excluded_file_name (ep->excluded, bname)))
193 break;
194 }
195 }
196
197 free (bname);
198
199 return result;
200 }
201
202 static void
203 cvs_addfn (struct exclude *ex, char const *pattern, int options,
204 MAYBE_UNUSED void *data)
205 {
206 struct wordsplit ws;
207 size_t i;
208
209 options |= EXCLUDE_ALLOC;
210 if (wordsplit (pattern, &ws,
211 WRDSF_NOVAR | WRDSF_NOCMD | WRDSF_SQUEEZE_DELIMS))
212 return;
213 for (i = 0; i < ws.ws_wordc; i++)
214 add_exclude (ex, ws.ws_wordv[i], options);
215 wordsplit_free (&ws);
216 }
217
218 static void
219 git_addfn (struct exclude *ex, char const *pattern, int options,
220 MAYBE_UNUSED void *data)
221 {
222 while (isspace (*pattern))
223 ++pattern;
224 if (*pattern == 0 || *pattern == '#')
225 return;
226 if (*pattern == '\\' && pattern[1] == '#')
227 ++pattern;
228 add_exclude (ex, pattern, options);
229 }
230
231 static void
232 bzr_addfn (struct exclude *ex, char const *pattern, int options,
233 MAYBE_UNUSED void *data)
234 {
235 while (isspace (*pattern))
236 ++pattern;
237 if (*pattern == 0 || *pattern == '#')
238 return;
239 if (*pattern == '!')
240 {
241 if (*++pattern == '!')
242 ++pattern;
243 else
244 options |= EXCLUDE_INCLUDE;
245 }
246 /* FIXME: According to the docs, globbing patterns are rsync-style,
247 and regexps are perl-style. */
248 if (strncmp (pattern, "RE:", 3) == 0)
249 {
250 pattern += 3;
251 options &= ~EXCLUDE_WILDCARDS;
252 options |= EXCLUDE_REGEX;
253 }
254 add_exclude (ex, pattern, options);
255 }
256
257 static void *
258 hg_initfn (void *data)
259 {
260 static int hg_options;
261 int *hgopt = data ? data : &hg_options;
262 *hgopt = EXCLUDE_REGEX;
263 return hgopt;
264 }
265
266 static void
267 hg_addfn (struct exclude *ex, char const *pattern, int options, void *data)
268 {
269 int *hgopt = data;
270 size_t len;
271
272 while (isspace (*pattern))
273 ++pattern;
274 if (*pattern == 0 || *pattern == '#')
275 return;
276 if (strncmp (pattern, "syntax:", 7) == 0)
277 {
278 for (pattern += 7; isspace (*pattern); ++pattern)
279 ;
280 if (strcmp (pattern, "regexp") == 0)
281 /* FIXME: Regexps must be perl-style */
282 *hgopt = EXCLUDE_REGEX;
283 else if (strcmp (pattern, "glob") == 0)
284 *hgopt = EXCLUDE_WILDCARDS;
285 /* Ignore unknown syntax */
286 return;
287 }
288
289 len = strlen(pattern);
290 if (pattern[len-1] == '/')
291 {
292 char *p;
293
294 --len;
295 p = xmalloc (len+1);
296 memcpy (p, pattern, len);
297 p[len] = 0;
298 pattern = p;
299 exclude_add_pattern_buffer (ex, p);
300 options |= FNM_LEADING_DIR|EXCLUDE_ALLOC;
301 }
302
303 add_exclude (ex, pattern,
304 ((*hgopt == EXCLUDE_REGEX)
305 ? (options & ~EXCLUDE_WILDCARDS)
306 : (options & ~EXCLUDE_REGEX)) | *hgopt);
307 }
308
309 static struct vcs_ignore_file vcs_ignore_files[] = {
310 { ".cvsignore", EXCL_NON_RECURSIVE, cvs_addfn, NULL, NULL },
311 { ".gitignore", 0, git_addfn, NULL, NULL },
312 { ".bzrignore", 0, bzr_addfn, NULL, NULL },
313 { ".hgignore", 0, hg_addfn, hg_initfn, NULL },
314 { NULL, 0, git_addfn, NULL, NULL }
315 };
316
317 static struct vcs_ignore_file *
318 get_vcs_ignore_file (const char *name)
319 {
320 struct vcs_ignore_file *p;
321
322 for (p = vcs_ignore_files; p->filename; p++)
323 if (strcmp (p->filename, name) == 0)
324 break;
325
326 return p;
327 }
328
329 void
330 exclude_vcs_ignores (void)
331 {
332 struct vcs_ignore_file *p;
333
334 for (p = vcs_ignore_files; p->filename; p++)
335 excfile_add (p->filename, EXCL_DEFAULT);
336 }