]> wimlib.net Git - wimlib/blob - src/ntfs-apply.c
Various code cleanups
[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 wim_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 wim_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 wim_inode *inode = dentry->d_inode;
92         struct wim_lookup_table_entry *lte;
93
94         DEBUG("Writing %u NTFS data stream%s for `%s'",
95               inode->i_num_ads + 1,
96               (inode->i_num_ads == 0 ? "" : "s"),
97               dentry->full_path_utf8);
98
99         lte = inode->i_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->i_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->i_ads_entries[stream_idx].stream_name;
156                 stream_name_len = inode->i_ads_entries[stream_idx].stream_name_len / 2;
157                 lte = inode->i_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 wim_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->i_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->i_extracted_file.
199  *
200  * Return 0 on success, nonzero on failure.
201  */
202 static int apply_ntfs_hardlink(const struct wim_dentry *from_dentry,
203                                const struct wim_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->i_extracted_file);
222
223         to_ni = ntfs_pathname_to_inode(vol, NULL, inode->i_extracted_file);
224         if (!to_ni) {
225                 ERROR_WITH_ERRNO("Could not find NTFS inode for `%s'",
226                                  inode->i_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->i_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 wim_dentry *dentry,
265                                         const WIMStruct *w)
266 {
267         int ret;
268         struct SECURITY_CONTEXT ctx;
269         u32 attributes_le32;
270         const struct wim_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->i_attributes);
276
277         attributes_le32 = cpu_to_le32(inode->i_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->i_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->i_security_id < sd->num_entries);
295                 desc = (const char *)sd->descriptors[inode->i_security_id];
296                 DEBUG("Applying security descriptor %d to `%s'",
297                       inode->i_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->i_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 wim_dentry *dentry,
317                               union wimlib_progress_info *progress_info)
318 {
319         struct wim_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->i_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 wim_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 wim_dentry *dentry,
371                                          ntfs_inode **dir_ni_p,
372                                          struct apply_args *args)
373 {
374         struct wim_dentry *other;
375         struct wim_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                 int ret;
394                 ntfs_volume *vol = (*dir_ni_p)->vol;
395
396                 DEBUG("pre-applying DOS name `%s'",
397                       dentry_with_dos_name->full_path_utf8);
398                 ret = do_apply_dentry_ntfs(dentry_with_dos_name,
399                                            *dir_ni_p, args);
400                 if (ret != 0)
401                         return ret;
402
403                 *dir_ni_p = dentry_open_parent_ni(dentry, vol);
404                 if (!*dir_ni_p)
405                         return WIMLIB_ERR_NTFS_3G;
406         }
407         return 0;
408 }
409
410 /*
411  * Applies a WIM dentry to a NTFS filesystem.
412  *
413  * @dentry:  The WIM dentry to apply
414  * @dir_ni:  The NTFS inode for the parent directory
415  *
416  * @return:  0 on success; nonzero on failure.
417  */
418 static int do_apply_dentry_ntfs(struct wim_dentry *dentry, ntfs_inode *dir_ni,
419                                 struct apply_args *args)
420 {
421         int ret = 0;
422         mode_t type;
423         ntfs_inode *ni = NULL;
424         ntfs_volume *vol = dir_ni->vol;
425         struct wim_inode *inode = dentry->d_inode;
426         dentry->is_extracted = 1;
427
428         if (inode->i_attributes & FILE_ATTRIBUTE_DIRECTORY) {
429                 type = S_IFDIR;
430         } else {
431                 /* If this dentry is hard-linked to any other dentries in the
432                  * same directory, make sure to apply the one (if any) with a
433                  * DOS name first.  Otherwise, NTFS-3g might not assign the file
434                  * names correctly. */
435                 if (dentry->short_name_len == 0) {
436                         ret = preapply_dentry_with_dos_name(dentry,
437                                                             &dir_ni, args);
438                         if (ret != 0)
439                                 return ret;
440                 }
441                 type = S_IFREG;
442                 if (inode->i_nlink > 1) {
443                         /* Inode has multiple dentries referencing it. */
444
445                         if (inode->i_extracted_file) {
446                                 /* Already extracted another dentry in the hard
447                                  * link group.  Make a hard link instead of
448                                  * extracting the file data. */
449                                 ret = apply_ntfs_hardlink(dentry, inode,
450                                                           &dir_ni);
451                                 if (ret == 0)
452                                         goto out_set_dos_name;
453                                 else
454                                         goto out_close_dir_ni;
455                         } else {
456                                 /* None of the dentries of this inode have been
457                                  * extracted yet, so go ahead and extract the
458                                  * first one. */
459                                 FREE(inode->i_extracted_file);
460                                 inode->i_extracted_file = STRDUP(dentry->full_path_utf8);
461                                 if (!inode->i_extracted_file) {
462                                         ret = WIMLIB_ERR_NOMEM;
463                                         goto out_close_dir_ni;
464                                 }
465                         }
466                 }
467         }
468
469         /* Create a NTFS directory or file.
470          *
471          * Note: For symbolic links that are not directory junctions, S_IFREG is
472          * passed here, since the reparse data and file attributes are set
473          * later. */
474         ni = ntfs_create(dir_ni, 0, (ntfschar*)dentry->file_name,
475                          dentry->file_name_len / 2, type);
476
477         if (!ni) {
478                 ERROR_WITH_ERRNO("Could not create NTFS inode for `%s'",
479                                  dentry->full_path_utf8);
480                 ret = WIMLIB_ERR_NTFS_3G;
481                 goto out_close_dir_ni;
482         }
483
484         /* Write the data streams, unless this is a directory or reparse point
485          * */
486         if (!(inode->i_attributes & (FILE_ATTRIBUTE_REPARSE_POINT |
487                                    FILE_ATTRIBUTE_DIRECTORY))) {
488                 ret = write_ntfs_data_streams(ni, dentry, &args->progress);
489                 if (ret != 0)
490                         goto out_close_dir_ni;
491         }
492
493
494         ret = apply_file_attributes_and_security_data(ni, dir_ni, dentry,
495                                                       args->w);
496         if (ret != 0)
497                 goto out_close_dir_ni;
498
499         if (inode->i_attributes & FILE_ATTR_REPARSE_POINT) {
500                 ret = apply_reparse_data(ni, dentry, &args->progress);
501                 if (ret != 0)
502                         goto out_close_dir_ni;
503         }
504
505 out_set_dos_name:
506         /* Set DOS (short) name if given */
507         if (dentry->short_name_len != 0) {
508
509                 char *short_name_utf8;
510                 size_t short_name_utf8_len;
511                 ret = utf16_to_utf8(dentry->short_name,
512                                     dentry->short_name_len,
513                                     &short_name_utf8,
514                                     &short_name_utf8_len);
515                 if (ret != 0)
516                         goto out_close_dir_ni;
517
518                 if (!ni) {
519                         /* Hardlink was made; linked inode needs to be looked up
520                          * again.  */
521                         ni = ntfs_pathname_to_inode(vol, dir_ni,
522                                                     dentry->file_name_utf8);
523                         if (!ni) {
524                                 ERROR_WITH_ERRNO("Could not find NTFS inode for `%s'",
525                                                  dentry->full_path_utf8);
526                                 FREE(short_name_utf8);
527                                 ret = WIMLIB_ERR_NTFS_3G;
528                                 goto out_close_dir_ni;
529                         }
530                 }
531
532                 DEBUG("Setting short (DOS) name of `%s' to %s",
533                       dentry->full_path_utf8, short_name_utf8);
534
535                 ret = ntfs_set_ntfs_dos_name(ni, dir_ni, short_name_utf8,
536                                              short_name_utf8_len, 0);
537                 FREE(short_name_utf8);
538                 if (ret != 0) {
539                         ERROR_WITH_ERRNO("Could not set DOS (short) name for `%s'",
540                                          dentry->full_path_utf8);
541                         ret = WIMLIB_ERR_NTFS_3G;
542                 }
543                 /* inodes have been closed by ntfs_set_ntfs_dos_name(). */
544                 return ret;
545         }
546
547 out_close_dir_ni:
548         if (dir_ni) {
549                 if (ni) {
550                         if (ntfs_inode_close_in_dir(ni, dir_ni)) {
551                                 if (ret == 0)
552                                         ret = WIMLIB_ERR_NTFS_3G;
553                                 ERROR_WITH_ERRNO("Failed to close inode for `%s'",
554                                                  dentry->full_path_utf8);
555                         }
556                 }
557                 if (ntfs_inode_close(dir_ni)) {
558                         if (ret == 0)
559                                 ret = WIMLIB_ERR_NTFS_3G;
560                         ERROR_WITH_ERRNO("Failed to close inode of directory "
561                                          "containing `%s'", dentry->full_path_utf8);
562                 }
563         } else {
564                 wimlib_assert(ni == NULL);
565         }
566         return ret;
567 }
568
569 static int apply_root_dentry_ntfs(const struct wim_dentry *dentry,
570                                   ntfs_volume *vol, const WIMStruct *w)
571 {
572         ntfs_inode *ni;
573         int ret = 0;
574
575         ni = ntfs_pathname_to_inode(vol, NULL, "/");
576         if (!ni) {
577                 ERROR_WITH_ERRNO("Could not find root NTFS inode");
578                 return WIMLIB_ERR_NTFS_3G;
579         }
580         ret = apply_file_attributes_and_security_data(ni, ni, dentry, w);
581         if (ntfs_inode_close(ni) != 0) {
582                 ERROR_WITH_ERRNO("Failed to close NTFS inode for root "
583                                  "directory");
584                 ret = WIMLIB_ERR_NTFS_3G;
585         }
586         return ret;
587 }
588
589 /* Applies a WIM dentry to the NTFS volume */
590 int apply_dentry_ntfs(struct wim_dentry *dentry, void *arg)
591 {
592         struct apply_args *args = arg;
593         ntfs_volume *vol = args->vol;
594         WIMStruct *w = args->w;
595         ntfs_inode *dir_ni;
596
597         if (dentry_is_root(dentry))
598                 return apply_root_dentry_ntfs(dentry, vol, w);
599
600         dir_ni = dentry_open_parent_ni(dentry, vol);
601         if (dir_ni)
602                 return do_apply_dentry_ntfs(dentry, dir_ni, arg);
603         else
604                 return WIMLIB_ERR_NTFS_3G;
605 }
606
607 /* Transfers the 100-nanosecond precision timestamps from a WIM dentry to a NTFS
608  * inode */
609 int apply_dentry_timestamps_ntfs(struct wim_dentry *dentry, void *arg)
610 {
611         struct apply_args *args = arg;
612         ntfs_volume *vol = args->vol;
613         u8 *p;
614         u8 buf[24];
615         ntfs_inode *ni;
616         int ret;
617
618         DEBUG("Setting timestamps on `%s'", dentry->full_path_utf8);
619
620         ni = ntfs_pathname_to_inode(vol, NULL, dentry->full_path_utf8);
621         if (!ni) {
622                 ERROR_WITH_ERRNO("Could not find NTFS inode for `%s'",
623                                  dentry->full_path_utf8);
624                 return WIMLIB_ERR_NTFS_3G;
625         }
626
627         p = buf;
628         p = put_u64(p, dentry->d_inode->i_creation_time);
629         p = put_u64(p, dentry->d_inode->i_last_write_time);
630         p = put_u64(p, dentry->d_inode->i_last_access_time);
631         ret = ntfs_inode_set_times(ni, (const char*)buf, 3 * sizeof(u64), 0);
632         if (ret != 0) {
633                 ERROR_WITH_ERRNO("Failed to set NTFS timestamps on `%s'",
634                                  dentry->full_path_utf8);
635                 ret = WIMLIB_ERR_NTFS_3G;
636         }
637
638         if (ntfs_inode_close(ni) != 0) {
639                 if (ret == 0)
640                         ret = WIMLIB_ERR_NTFS_3G;
641                 ERROR_WITH_ERRNO("Failed to close NTFS inode for `%s'",
642                                  dentry->full_path_utf8);
643         }
644         return ret;
645 }