c00be57fe743e9740b884b81066ea7a0f8aaf38d
[wimlib] / src / ntfs-apply.c
1 /*
2  * ntfs-apply.c
3  *
4  * Apply a WIM image to a NTFS volume.  Restore as much information as possible,
5  * including security data, file attributes, DOS names, and alternate data
6  * streams.
7  */
8
9 /*
10  * Copyright (C) 2012 Eric Biggers
11  *
12  * This file is part of wimlib, a library for working with WIM files.
13  *
14  * wimlib is free software; you can redistribute it and/or modify it under the
15  * terms of the GNU General Public License as published by the Free
16  * Software Foundation; either version 3 of the License, or (at your option)
17  * any later version.
18  *
19  * wimlib is distributed in the hope that it will be useful, but WITHOUT ANY
20  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
21  * A PARTICULAR PURPOSE. See the GNU General Public License for more
22  * details.
23  *
24  * You should have received a copy of the GNU General Public License
25  * along with wimlib; if not, see http://www.gnu.org/licenses/.
26  */
27
28
29 #include "config.h"
30
31 #include <ntfs-3g/endians.h>
32 #include <ntfs-3g/types.h>
33
34 #include "wimlib_internal.h"
35 #include "buffer_io.h"
36 #include "dentry.h"
37 #include "lookup_table.h"
38
39 #include <ntfs-3g/attrib.h>
40 #include <ntfs-3g/security.h> /* security.h before xattrs.h */
41 #include <ntfs-3g/reparse.h>
42 #include <ntfs-3g/xattrs.h>
43 #include <string.h>
44
45 static int extract_wim_chunk_to_ntfs_attr(const u8 *buf, size_t len,
46                                           u64 offset, void *arg)
47 {
48         ntfs_attr *na = arg;
49         if (ntfs_attr_pwrite(na, offset, len, buf) == len) {
50                 return 0;
51         } else {
52                 ERROR_WITH_ERRNO("Error extracting WIM resource to NTFS attribute");
53                 return WIMLIB_ERR_WRITE;
54         }
55 }
56
57 /*
58  * Extracts a WIM resource to a NTFS attribute.
59  */
60 static int
61 extract_wim_resource_to_ntfs_attr(const struct lookup_table_entry *lte,
62                                   ntfs_attr *na)
63 {
64         return extract_wim_resource(lte, wim_resource_size(lte),
65                                     extract_wim_chunk_to_ntfs_attr, na);
66 }
67
68 /* Writes the data streams of a WIM inode to the data attributes of a NTFS
69  * inode.
70  *
71  * @ni:      The NTFS inode to which the streams are to be extracted.
72  *
73  * @dentry:  The WIM dentry being extracted.  The @d_inode member points to the
74  *           corresponding WIM inode that contains the streams being extracted.
75  *           The WIM dentry itself is only needed to provide a file path for
76  *           better error messages.
77  *
78  * @progress_info:  Progress information for the image application.  The number
79  *                  of extracted bytes will be incremented by the uncompressed
80  *                  size of each stream extracted.
81  *
82  * Returns 0 on success, nonzero on failure.
83  */
84 static int write_ntfs_data_streams(ntfs_inode *ni, const struct dentry *dentry,
85                                    union wimlib_progress_info *progress_info)
86 {
87         int ret = 0;
88         unsigned stream_idx = 0;
89         ntfschar *stream_name = AT_UNNAMED;
90         u32 stream_name_len = 0;
91         const struct inode *inode = dentry->d_inode;
92         struct lookup_table_entry *lte;
93
94         DEBUG("Writing %u NTFS data stream%s for `%s'",
95               inode->num_ads + 1,
96               (inode->num_ads == 0 ? "" : "s"),
97               dentry->full_path_utf8);
98
99         lte = inode->lte;
100         while (1) {
101                 if (stream_name_len) {
102                         /* Create an empty named stream. */
103                         ret = ntfs_attr_add(ni, AT_DATA, stream_name,
104                                             stream_name_len, NULL, 0);
105                         if (ret != 0) {
106                                 ERROR_WITH_ERRNO("Failed to create name data "
107                                                  "stream for extracted file "
108                                                  "`%s'",
109                                                  dentry->full_path_utf8);
110                                 ret = WIMLIB_ERR_NTFS_3G;
111                                 break;
112
113                         }
114                 }
115
116                 /* If there's no lookup table entry, it's an empty stream.
117                  * Otherwise, open the attribute and extract the data. */
118                 if (lte) {
119                         ntfs_attr *na;
120
121                         na = ntfs_attr_open(ni, AT_DATA, stream_name, stream_name_len);
122                         if (!na) {
123                                 ERROR_WITH_ERRNO("Failed to open a data stream of "
124                                                  "extracted file `%s'",
125                                                  dentry->full_path_utf8);
126                                 ret = WIMLIB_ERR_NTFS_3G;
127                                 break;
128                         }
129
130                         /* The WIM lookup table entry provides the stream
131                          * length, so the NTFS attribute should be resized to
132                          * this length before starting to extract the data. */
133                         ret = ntfs_attr_truncate_solid(na, wim_resource_size(lte));
134                         if (ret != 0) {
135                                 ntfs_attr_close(na);
136                                 break;
137                         }
138
139                         /* Actually extract the stream */
140                         ret = extract_wim_resource_to_ntfs_attr(lte, na);
141
142                         /* Close the attribute */
143                         ntfs_attr_close(na);
144                         if (ret != 0)
145                                 break;
146
147                         /* Record the number of bytes of uncompressed data that
148                          * have been extracted. */
149                         progress_info->extract.completed_bytes += wim_resource_size(lte);
150                 }
151                 if (stream_idx == inode->num_ads) /* Has the last stream been extracted? */
152                         break;
153
154                 /* Get the name and lookup table entry for the next stream. */
155                 stream_name = (ntfschar*)inode->ads_entries[stream_idx].stream_name;
156                 stream_name_len = inode->ads_entries[stream_idx].stream_name_len / 2;
157                 lte = inode->ads_entries[stream_idx].lte;
158                 stream_idx++;
159         }
160         return ret;
161 }
162
163 /* Open the NTFS inode that corresponds to the parent of a WIM dentry.  Returns
164  * the opened inode, or NULL on failure. */
165 static ntfs_inode *dentry_open_parent_ni(const struct dentry *dentry,
166                                          ntfs_volume *vol)
167 {
168         char *p;
169         const char *dir_name;
170         ntfs_inode *dir_ni;
171         char orig;
172
173         p = dentry->full_path_utf8 + dentry->full_path_utf8_len;
174         do {
175                 p--;
176         } while (*p != '/');
177
178         orig = *p;
179         *p = '\0';
180         dir_name = dentry->full_path_utf8;
181         dir_ni = ntfs_pathname_to_inode(vol, NULL, dir_name);
182         if (!dir_ni) {
183                 ERROR_WITH_ERRNO("Could not find NTFS inode for `%s'",
184                                  dir_name);
185         }
186         *p = orig;
187         return dir_ni;
188 }
189
190 /*
191  * Makes a NTFS hard link.
192  *
193  * The hard link is named @from_dentry->file_name and is located under the
194  * directory specified by @dir_ni, and it is made to point to the previously
195  * extracted file located at @inode->extracted_file.
196  *
197  * Or, in other words, this adds a new name @from_dentry->full_path_utf8 to an
198  * existing NTFS inode which already has a name @inode->extracted_file.
199  *
200  * Return 0 on success, nonzero on failure.
201  */
202 static int apply_ntfs_hardlink(const struct dentry *from_dentry,
203                                const struct inode *inode,
204                                ntfs_inode **dir_ni_p)
205 {
206         int ret;
207         ntfs_inode *to_ni;
208         ntfs_inode *dir_ni;
209         ntfs_volume *vol;
210
211         dir_ni = *dir_ni_p;
212         vol = dir_ni->vol;
213         ret = ntfs_inode_close(dir_ni);
214         *dir_ni_p = NULL;
215         if (ret != 0) {
216                 ERROR_WITH_ERRNO("Error closing directory");
217                 return WIMLIB_ERR_NTFS_3G;
218         }
219
220         DEBUG("Extracting NTFS hard link `%s' => `%s'",
221               from_dentry->full_path_utf8, inode->extracted_file);
222
223         to_ni = ntfs_pathname_to_inode(vol, NULL, inode->extracted_file);
224         if (!to_ni) {
225                 ERROR_WITH_ERRNO("Could not find NTFS inode for `%s'",
226                                  inode->extracted_file);
227                 return WIMLIB_ERR_NTFS_3G;
228         }
229
230         dir_ni = dentry_open_parent_ni(from_dentry, vol);
231         if (!dir_ni) {
232                 ntfs_inode_close(to_ni);
233                 return WIMLIB_ERR_NTFS_3G;
234         }
235
236         *dir_ni_p = dir_ni;
237
238         ret = ntfs_link(to_ni, dir_ni,
239                         (ntfschar*)from_dentry->file_name,
240                         from_dentry->file_name_len / 2);
241         if (ntfs_inode_close_in_dir(to_ni, dir_ni) || ret != 0) {
242                 ERROR_WITH_ERRNO("Could not create hard link `%s' => `%s'",
243                                  from_dentry->full_path_utf8,
244                                  inode->extracted_file);
245                 ret = WIMLIB_ERR_NTFS_3G;
246         }
247         return ret;
248 }
249
250 /* Transfers file attributes and possibly a security descriptor from a WIM inode
251  * to a NTFS inode.
252  *
253  * @ni:      The NTFS inode to apply the metadata to.
254  * @dir_ni:  The NTFS inode for a directory containing @ni.
255  * @dentry:  The WIM dentry whose inode contains the metadata to apply.
256  * @w:       The WIMStruct for the WIM, through which the table of security
257  *              descriptors can be accessed.
258  *
259  * Returns 0 on success, nonzero on failure.
260  */
261 static int
262 apply_file_attributes_and_security_data(ntfs_inode *ni,
263                                         ntfs_inode *dir_ni,
264                                         const struct dentry *dentry,
265                                         const WIMStruct *w)
266 {
267         int ret;
268         struct SECURITY_CONTEXT ctx;
269         u32 attributes_le32;
270         const struct inode *inode;
271
272         inode = dentry->d_inode;
273
274         DEBUG("Setting NTFS file attributes on `%s' to %#"PRIx32,
275               dentry->full_path_utf8, inode->attributes);
276
277         attributes_le32 = cpu_to_le32(inode->attributes);
278         memset(&ctx, 0, sizeof(ctx));
279         ctx.vol = ni->vol;
280         ret = ntfs_xattr_system_setxattr(&ctx, XATTR_NTFS_ATTRIB,
281                                          ni, dir_ni,
282                                          (const char*)&attributes_le32,
283                                          sizeof(u32), 0);
284         if (ret != 0) {
285                 ERROR("Failed to set NTFS file attributes on `%s'",
286                        dentry->full_path_utf8);
287                 return WIMLIB_ERR_NTFS_3G;
288         }
289         if (inode->security_id != -1) {
290                 const char *desc;
291                 const struct wim_security_data *sd;
292
293                 sd = wim_const_security_data(w);
294                 wimlib_assert(inode->security_id < sd->num_entries);
295                 desc = (const char *)sd->descriptors[inode->security_id];
296                 DEBUG("Applying security descriptor %d to `%s'",
297                       inode->security_id, dentry->full_path_utf8);
298
299                 ret = ntfs_xattr_system_setxattr(&ctx, XATTR_NTFS_ACL,
300                                                  ni, dir_ni, desc,
301                                                  sd->sizes[inode->security_id], 0);
302
303                 if (ret != 0) {
304                         ERROR_WITH_ERRNO("Failed to set security data on `%s'",
305                                         dentry->full_path_utf8);
306                         return WIMLIB_ERR_NTFS_3G;
307                 }
308         }
309         return 0;
310 }
311
312 /*
313  * Transfers the reparse data from a WIM inode (which must represent a reparse
314  * point) to a NTFS inode.
315  */
316 static int apply_reparse_data(ntfs_inode *ni, const struct dentry *dentry,
317                               union wimlib_progress_info *progress_info)
318 {
319         struct lookup_table_entry *lte;
320         int ret = 0;
321
322         lte = inode_unnamed_lte_resolved(dentry->d_inode);
323
324         DEBUG("Applying reparse data to `%s'", dentry->full_path_utf8);
325
326         if (!lte) {
327                 ERROR("Could not find reparse data for `%s'",
328                       dentry->full_path_utf8);
329                 return WIMLIB_ERR_INVALID_DENTRY;
330         }
331
332         if (wim_resource_size(lte) >= 0xffff) {
333                 ERROR("Reparse data of `%s' is too long (%"PRIu64" bytes)",
334                       dentry->full_path_utf8, wim_resource_size(lte));
335                 return WIMLIB_ERR_INVALID_DENTRY;
336         }
337
338         u8 reparse_data_buf[8 + wim_resource_size(lte)];
339         u8 *p = reparse_data_buf;
340         p = put_u32(p, dentry->d_inode->reparse_tag); /* ReparseTag */
341         p = put_u16(p, wim_resource_size(lte)); /* ReparseDataLength */
342         p = put_u16(p, 0); /* Reserved */
343
344         ret = read_full_wim_resource(lte, p, 0);
345         if (ret != 0)
346                 return ret;
347
348         ret = ntfs_set_ntfs_reparse_data(ni, (char*)reparse_data_buf,
349                                          wim_resource_size(lte) + 8, 0);
350         if (ret != 0) {
351                 ERROR_WITH_ERRNO("Failed to set NTFS reparse data on `%s'",
352                                  dentry->full_path_utf8);
353                 return WIMLIB_ERR_NTFS_3G;
354         }
355         progress_info->extract.completed_bytes += wim_resource_size(lte);
356         return 0;
357 }
358
359 static int do_apply_dentry_ntfs(struct dentry *dentry, ntfs_inode *dir_ni,
360                                 struct apply_args *args);
361
362 /*
363  * If @dentry is part of a hard link group, search for hard-linked dentries in
364  * the same directory that have a nonempty DOS (short) filename.  There should
365  * be exactly 0 or 1 such dentries.  If there is 1, extract that dentry first,
366  * so that the DOS name is correctly associated with the corresponding long name
367  * in the Win32 namespace, and not any of the additional names in the POSIX
368  * namespace created from hard links.
369  */
370 static int preapply_dentry_with_dos_name(struct dentry *dentry,
371                                          ntfs_inode **dir_ni_p,
372                                          struct apply_args *args)
373 {
374         struct dentry *other;
375         struct dentry *dentry_with_dos_name;
376
377         dentry_with_dos_name = NULL;
378         inode_for_each_dentry(other, dentry->d_inode) {
379                 if (other != dentry && (dentry->parent == other->parent)
380                     && other->short_name_len)
381                 {
382                         if (dentry_with_dos_name) {
383                                 ERROR("Found multiple DOS names for file `%s' "
384                                       "in the same directory",
385                                       dentry_with_dos_name->full_path_utf8);
386                                 return WIMLIB_ERR_INVALID_DENTRY;
387                         }
388                         dentry_with_dos_name = other;
389                 }
390         }
391         /* If there's a dentry with a DOS name, extract it first */
392         if (dentry_with_dos_name && !dentry_with_dos_name->is_extracted) {
393                 char *p;
394                 const char *dir_name;
395                 char orig;
396                 int ret;
397                 ntfs_volume *vol = (*dir_ni_p)->vol;
398
399                 DEBUG("pre-applying DOS name `%s'",
400                       dentry_with_dos_name->full_path_utf8);
401                 ret = do_apply_dentry_ntfs(dentry_with_dos_name,
402                                            *dir_ni_p, args);
403                 if (ret != 0)
404                         return ret;
405
406                 *dir_ni_p = dentry_open_parent_ni(dentry, vol);
407                 if (!*dir_ni_p)
408                         return WIMLIB_ERR_NTFS_3G;
409         }
410         return 0;
411 }
412
413 /*
414  * Applies a WIM dentry to a NTFS filesystem.
415  *
416  * @dentry:  The WIM dentry to apply
417  * @dir_ni:  The NTFS inode for the parent directory
418  *
419  * @return:  0 on success; nonzero on failure.
420  */
421 static int do_apply_dentry_ntfs(struct dentry *dentry, ntfs_inode *dir_ni,
422                                 struct apply_args *args)
423 {
424         int ret = 0;
425         mode_t type;
426         ntfs_inode *ni = NULL;
427         ntfs_volume *vol = dir_ni->vol;
428         struct inode *inode = dentry->d_inode;
429         dentry->is_extracted = 1;
430
431         if (inode->attributes & FILE_ATTRIBUTE_DIRECTORY) {
432                 type = S_IFDIR;
433         } else {
434                 /* If this dentry is hard-linked to any other dentries in the
435                  * same directory, make sure to apply the one (if any) with a
436                  * DOS name first.  Otherwise, NTFS-3g might not assign the file
437                  * names correctly. */
438                 if (dentry->short_name_len == 0) {
439                         ret = preapply_dentry_with_dos_name(dentry,
440                                                             &dir_ni, args);
441                         if (ret != 0)
442                                 return ret;
443                 }
444                 type = S_IFREG;
445                 if (inode->link_count > 1) {
446                         /* Inode has multiple dentries referencing it. */
447
448                         if (inode->extracted_file) {
449                                 /* Already extracted another dentry in the hard
450                                  * link group.  Make a hard link instead of
451                                  * extracting the file data. */
452                                 ret = apply_ntfs_hardlink(dentry, inode,
453                                                           &dir_ni);
454                                 if (ret == 0)
455                                         goto out_set_dos_name;
456                                 else
457                                         goto out_close_dir_ni;
458                         } else {
459                                 /* None of the dentries of this inode have been
460                                  * extracted yet, so go ahead and extract the
461                                  * first one. */
462                                 FREE(inode->extracted_file);
463                                 inode->extracted_file = STRDUP(dentry->full_path_utf8);
464                                 if (!inode->extracted_file) {
465                                         ret = WIMLIB_ERR_NOMEM;
466                                         goto out_close_dir_ni;
467                                 }
468                         }
469                 }
470         }
471
472         /* Create a NTFS directory or file.
473          *
474          * Note: For symbolic links that are not directory junctions, S_IFREG is
475          * passed here, since the reparse data and file attributes are set
476          * later. */
477         ni = ntfs_create(dir_ni, 0, (ntfschar*)dentry->file_name,
478                          dentry->file_name_len / 2, type);
479
480         if (!ni) {
481                 ERROR_WITH_ERRNO("Could not create NTFS inode for `%s'",
482                                  dentry->full_path_utf8);
483                 ret = WIMLIB_ERR_NTFS_3G;
484                 goto out_close_dir_ni;
485         }
486
487         /* Write the data streams, unless this is a directory or reparse point
488          * */
489         if (!(inode->attributes & (FILE_ATTRIBUTE_REPARSE_POINT |
490                                    FILE_ATTRIBUTE_DIRECTORY))) {
491                 ret = write_ntfs_data_streams(ni, dentry, &args->progress);
492                 if (ret != 0)
493                         goto out_close_dir_ni;
494         }
495
496
497         ret = apply_file_attributes_and_security_data(ni, dir_ni, dentry,
498                                                       args->w);
499         if (ret != 0)
500                 goto out_close_dir_ni;
501
502         if (inode->attributes & FILE_ATTR_REPARSE_POINT) {
503                 ret = apply_reparse_data(ni, dentry, &args->progress);
504                 if (ret != 0)
505                         goto out_close_dir_ni;
506         }
507
508 out_set_dos_name:
509         /* Set DOS (short) name if given */
510         if (dentry->short_name_len != 0) {
511
512                 char *short_name_utf8;
513                 size_t short_name_utf8_len;
514                 ret = utf16_to_utf8(dentry->short_name,
515                                     dentry->short_name_len,
516                                     &short_name_utf8,
517                                     &short_name_utf8_len);
518                 if (ret != 0)
519                         goto out_close_dir_ni;
520
521                 if (!ni) {
522                         /* Hardlink was made; linked inode needs to be looked up
523                          * again.  */
524                         ni = ntfs_pathname_to_inode(vol, dir_ni,
525                                                     dentry->file_name_utf8);
526                         if (!ni) {
527                                 ERROR_WITH_ERRNO("Could not find NTFS inode for `%s'",
528                                                  dentry->full_path_utf8);
529                                 FREE(short_name_utf8);
530                                 ret = WIMLIB_ERR_NTFS_3G;
531                                 goto out_close_dir_ni;
532                         }
533                 }
534
535                 DEBUG("Setting short (DOS) name of `%s' to %s",
536                       dentry->full_path_utf8, short_name_utf8);
537
538                 ret = ntfs_set_ntfs_dos_name(ni, dir_ni, short_name_utf8,
539                                              short_name_utf8_len, 0);
540                 FREE(short_name_utf8);
541                 if (ret != 0) {
542                         ERROR_WITH_ERRNO("Could not set DOS (short) name for `%s'",
543                                          dentry->full_path_utf8);
544                         ret = WIMLIB_ERR_NTFS_3G;
545                 }
546                 /* inodes have been closed by ntfs_set_ntfs_dos_name(). */
547                 return ret;
548         }
549
550 out_close_dir_ni:
551         if (dir_ni) {
552                 if (ni) {
553                         if (ntfs_inode_close_in_dir(ni, dir_ni)) {
554                                 if (ret == 0)
555                                         ret = WIMLIB_ERR_NTFS_3G;
556                                 ERROR_WITH_ERRNO("Failed to close inode for `%s'",
557                                                  dentry->full_path_utf8);
558                         }
559                 }
560                 if (ntfs_inode_close(dir_ni)) {
561                         if (ret == 0)
562                                 ret = WIMLIB_ERR_NTFS_3G;
563                         ERROR_WITH_ERRNO("Failed to close inode of directory "
564                                          "containing `%s'", dentry->full_path_utf8);
565                 }
566         } else {
567                 wimlib_assert(ni == NULL);
568         }
569         return ret;
570 }
571
572 static int apply_root_dentry_ntfs(const struct dentry *dentry,
573                                   ntfs_volume *vol, const WIMStruct *w)
574 {
575         ntfs_inode *ni;
576         int ret = 0;
577
578         ni = ntfs_pathname_to_inode(vol, NULL, "/");
579         if (!ni) {
580                 ERROR_WITH_ERRNO("Could not find root NTFS inode");
581                 return WIMLIB_ERR_NTFS_3G;
582         }
583         ret = apply_file_attributes_and_security_data(ni, ni, dentry, w);
584         if (ntfs_inode_close(ni) != 0) {
585                 ERROR_WITH_ERRNO("Failed to close NTFS inode for root "
586                                  "directory");
587                 ret = WIMLIB_ERR_NTFS_3G;
588         }
589         return ret;
590 }
591
592 /* Applies a WIM dentry to the NTFS volume */
593 int apply_dentry_ntfs(struct dentry *dentry, void *arg)
594 {
595         struct apply_args *args = arg;
596         ntfs_volume *vol = args->vol;
597         WIMStruct *w = args->w;
598         ntfs_inode *dir_ni;
599
600         if (dentry_is_root(dentry))
601                 return apply_root_dentry_ntfs(dentry, vol, w);
602
603         dir_ni = dentry_open_parent_ni(dentry, vol);
604         if (dir_ni)
605                 return do_apply_dentry_ntfs(dentry, dir_ni, arg);
606         else
607                 return WIMLIB_ERR_NTFS_3G;
608 }
609
610 /* Transfers the 100-nanosecond precision timestamps from a WIM dentry to a NTFS
611  * inode */
612 int apply_dentry_timestamps_ntfs(struct dentry *dentry, void *arg)
613 {
614         struct apply_args *args = arg;
615         ntfs_volume *vol = args->vol;
616         u8 *p;
617         u8 buf[24];
618         ntfs_inode *ni;
619         int ret;
620
621         DEBUG("Setting timestamps on `%s'", dentry->full_path_utf8);
622
623         ni = ntfs_pathname_to_inode(vol, NULL, dentry->full_path_utf8);
624         if (!ni) {
625                 ERROR_WITH_ERRNO("Could not find NTFS inode for `%s'",
626                                  dentry->full_path_utf8);
627                 return WIMLIB_ERR_NTFS_3G;
628         }
629
630         p = buf;
631         p = put_u64(p, dentry->d_inode->creation_time);
632         p = put_u64(p, dentry->d_inode->last_write_time);
633         p = put_u64(p, dentry->d_inode->last_access_time);
634         ret = ntfs_inode_set_times(ni, (const char*)buf, 3 * sizeof(u64), 0);
635         if (ret != 0) {
636                 ERROR_WITH_ERRNO("Failed to set NTFS timestamps on `%s'",
637                                  dentry->full_path_utf8);
638                 ret = WIMLIB_ERR_NTFS_3G;
639         }
640
641         if (ntfs_inode_close(ni) != 0) {
642                 if (ret == 0)
643                         ret = WIMLIB_ERR_NTFS_3G;
644                 ERROR_WITH_ERRNO("Failed to close NTFS inode for `%s'",
645                                  dentry->full_path_utf8);
646         }
647         return ret;
648 }