/** * ntfscmp - Part of the Linux-NTFS project. * * Copyright (c) 2005-2006 Szabolcs Szakacsits * Copyright (c) 2005 Anton Altaparmakov * Copyright (c) 2007 Yura Pakhuchiy * * This utility compare two NTFS volumes. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program (in the main directory of the Linux-NTFS * distribution in the file COPYING); if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "config.h" #include #include #include #include #include #include #include #include "mst.h" #include "support.h" #include "utils.h" #include "misc.h" /* #include "version.h" */ static const char *EXEC_NAME = "ntfscmp"; static const char *invalid_ntfs_msg = "Apparently device '%s' doesn't have a valid NTFS.\n" "Maybe you selected the wrong partition? Or the whole disk instead of a\n" "partition (e.g. /dev/hda, not /dev/hda1)?\n"; static const char *corrupt_volume_msg = "Apparently you have a corrupted NTFS. Please run the filesystem checker\n" "on Windows by invoking chkdsk /f. Don't forget the /f (force) parameter,\n" "it's important! You probably also need to reboot Windows to take effect.\n"; static const char *hibernated_volume_msg = "Apparently the NTFS partition is hibernated. Windows must be resumed and\n" "turned off properly\n"; static struct { int debug; int show_progress; int verbose; char *vol1; char *vol2; } opt; #define NTFS_PROGBAR 0x0001 #define NTFS_PROGBAR_SUPPRESS 0x0002 struct progress_bar { u64 start; u64 stop; int resolution; int flags; float unit; }; /* WARNING: don't modify the text, external tools grep for it */ #define ERR_PREFIX "ERROR" #define PERR_PREFIX ERR_PREFIX "(%d): " #define NERR_PREFIX ERR_PREFIX ": " __attribute__((format(printf, 2, 3))) static void perr_printf(int newline, const char *fmt, ...) { va_list ap; int eo = errno; fprintf(stdout, PERR_PREFIX, eo); va_start(ap, fmt); vfprintf(stdout, fmt, ap); va_end(ap); fprintf(stdout, ": %s", strerror(eo)); if (newline) fprintf(stdout, "\n"); fflush(stdout); fflush(stderr); } #define perr_print(...) perr_printf(0, __VA_ARGS__) #define perr_println(...) perr_printf(1, __VA_ARGS__) __attribute__((format(printf, 1, 2))) static void err_printf(const char *fmt, ...) { va_list ap; fprintf(stdout, NERR_PREFIX); va_start(ap, fmt); vfprintf(stdout, fmt, ap); va_end(ap); fflush(stdout); fflush(stderr); } /** * err_exit * * Print and error message and exit the program. */ __attribute__((noreturn)) __attribute__((format(printf, 1, 2))) static int err_exit(const char *fmt, ...) { va_list ap; fprintf(stdout, NERR_PREFIX); va_start(ap, fmt); vfprintf(stdout, fmt, ap); va_end(ap); fflush(stdout); fflush(stderr); exit(1); } /** * perr_exit * * Print and error message and exit the program */ __attribute__((noreturn)) __attribute__((format(printf, 1, 2))) static int perr_exit(const char *fmt, ...) { va_list ap; int eo = errno; fprintf(stdout, PERR_PREFIX, eo); va_start(ap, fmt); vfprintf(stdout, fmt, ap); va_end(ap); printf(": %s\n", strerror(eo)); fflush(stdout); fflush(stderr); exit(1); } /** * usage - Print a list of the parameters to the program * * Print a list of the parameters and options for the program. * * Return: none */ __attribute__((noreturn)) static void usage(void) { printf("\nUsage: %s [OPTIONS] DEVICE1 DEVICE2\n" " Compare two NTFS volumes and tell the differences.\n" "\n" " -P, --no-progress-bar Don't show progress bar\n" " -v, --verbose More output\n" " -h, --help Display this help\n" #ifdef DEBUG " -d, --debug Show debug information\n" #endif "\n", EXEC_NAME); printf("%s%s", ntfs_bugs, ntfs_home); exit(1); } static void parse_options(int argc, char **argv) { static const char *sopt = "-dhPv"; static const struct option lopt[] = { #ifdef DEBUG { "debug", no_argument, NULL, 'd' }, #endif { "help", no_argument, NULL, 'h' }, { "no-progress-bar", no_argument, NULL, 'P' }, { "verbose", no_argument, NULL, 'v' }, { NULL, 0, NULL, 0 } }; int c; memset(&opt, 0, sizeof(opt)); opt.show_progress = 1; while ((c = getopt_long(argc, argv, sopt, lopt, NULL)) != -1) { switch (c) { case 1: /* A non-option argument */ if (!opt.vol1) { opt.vol1 = argv[optind - 1]; } else if (!opt.vol2) { opt.vol2 = argv[optind - 1]; } else { err_printf("Too many arguments!\n"); usage(); } break; #ifdef DEBUG case 'd': opt.debug++; break; #endif case 'h': case '?': usage(); case 'P': opt.show_progress = 0; break; case 'v': opt.verbose++; break; default: err_printf("Unknown option '%s'.\n", argv[optind - 1]); usage(); break; } } if (opt.vol1 == NULL || opt.vol2 == NULL) { err_printf("You must specify exactly 2 volumes.\n"); usage(); } /* Redirect stderr to stdout, note fflush()es are essential! */ fflush(stdout); fflush(stderr); if (dup2(STDOUT_FILENO, STDERR_FILENO) == -1) { perror("Failed to redirect stderr to stdout"); exit(1); } fflush(stdout); fflush(stderr); #ifdef DEBUG if (!opt.debug) if (!freopen("/dev/null", "w", stderr)) perr_exit("Failed to redirect stderr to /dev/null"); #endif } static ntfs_attr_search_ctx *attr_get_search_ctx(ntfs_inode *ni) { ntfs_attr_search_ctx *ret; if ((ret = ntfs_attr_get_search_ctx(ni, NULL)) == NULL) perr_println("ntfs_attr_get_search_ctx"); return ret; } static void progress_init(struct progress_bar *p, u64 start, u64 stop, int flags) { p->start = start; p->stop = stop; p->unit = 100.0 / (stop - start); p->resolution = 100; p->flags = flags; } static void progress_update(struct progress_bar *p, u64 current) { float percent; if (!(p->flags & NTFS_PROGBAR)) return; if (p->flags & NTFS_PROGBAR_SUPPRESS) return; /* WARNING: don't modify the texts, external tools grep for them */ percent = p->unit * current; if (current != p->stop) { if ((current - p->start) % p->resolution) return; printf("%6.2f percent completed\r", percent); } else printf("100.00 percent completed\n"); fflush(stdout); } static u64 inumber(ntfs_inode *ni) { if (ni->nr_extents >= 0) return ni->mft_no; return ni->base_ni->mft_no; } static int inode_close(ntfs_inode *ni) { if (ni == NULL) return 0; if (ntfs_inode_close(ni)) { perr_println("ntfs_inode_close: inode %llu", (unsigned long long)inumber(ni)); return -1; } return 0; } static inline s64 get_nr_mft_records(ntfs_volume *vol) { return vol->mft_na->initialized_size >> vol->mft_record_size_bits; } #define NTFSCMP_OK 0 #define NTFSCMP_INODE_OPEN_ERROR 1 #define NTFSCMP_INODE_OPEN_IO_ERROR 2 #define NTFSCMP_INODE_OPEN_ENOENT_ERROR 3 #define NTFSCMP_EXTENSION_RECORD 4 #define NTFSCMP_INODE_CLOSE_ERROR 5 static const char *ntfscmp_errs[] = { "OK", "INODE_OPEN_ERROR", "INODE_OPEN_IO_ERROR", "INODE_OPEN_ENOENT_ERROR", "EXTENSION_RECORD", "INODE_CLOSE_ERROR", "" }; static const char *err2string(int err) { return ntfscmp_errs[err]; } static const char *pret2str(void *p) { if (p == NULL) return "FAILED"; return "OK"; } static int inode_open(ntfs_volume *vol, MFT_REF mref, ntfs_inode **ni) { *ni = ntfs_inode_open(vol, mref); if (*ni == NULL) { if (errno == EIO) return NTFSCMP_INODE_OPEN_IO_ERROR; if (errno == ENOENT) return NTFSCMP_INODE_OPEN_ENOENT_ERROR; perr_println("Reading inode %lld failed", (long long)mref); return NTFSCMP_INODE_OPEN_ERROR; } if ((*ni)->mrec->base_mft_record) { if (inode_close(*ni) != 0) return NTFSCMP_INODE_CLOSE_ERROR; return NTFSCMP_EXTENSION_RECORD; } return NTFSCMP_OK; } static ntfs_inode *base_inode(ntfs_attr_search_ctx *ctx) { if (ctx->base_ntfs_ino) return ctx->base_ntfs_ino; return ctx->ntfs_ino; } static void print_inode(u64 inum) { printf("Inode %llu ", (unsigned long long)inum); } static void print_inode_ni(ntfs_inode *ni) { print_inode(inumber(ni)); } static void print_attribute_type(ATTR_TYPES atype) { printf("attribute 0x%x", atype); } static void print_attribute_name(char *name) { if (name) printf(":%s", name); } #define GET_ATTR_NAME(a) \ ((ntfschar *)(((u8 *)(a)) + le16_to_cpu((a)->name_offset))), \ ((a)->name_length) static void free_name(char **name) { if (*name) { free(*name); *name = NULL; } } static char *get_attr_name(u64 mft_no, ATTR_TYPES atype, const ntfschar *uname, const int uname_len) { char *name = NULL; int name_len; if (atype == AT_END) return NULL; name_len = ntfs_ucstombs(uname, uname_len, &name, 0); if (name_len < 0) { perr_print("ntfs_ucstombs"); print_inode(mft_no); print_attribute_type(atype); puts(""); exit(1); } else if (name_len > 0) return name; free_name(&name); return NULL; } static char *get_attr_name_na(ntfs_attr *na) { return get_attr_name(inumber(na->ni), na->type, na->name, na->name_len); } static char *get_attr_name_ctx(ntfs_attr_search_ctx *ctx) { u64 mft_no = inumber(ctx->ntfs_ino); ATTR_TYPES atype = ctx->attr->type; return get_attr_name(mft_no, atype, GET_ATTR_NAME(ctx->attr)); } static void print_attribute(ATTR_TYPES atype, char *name) { print_attribute_type(atype); print_attribute_name(name); printf(" "); } static void print_na(ntfs_attr *na) { char *name = get_attr_name_na(na); print_inode_ni(na->ni); print_attribute(na->type, name); free_name(&name); } static void print_attribute_ctx(ntfs_attr_search_ctx *ctx) { char *name = get_attr_name_ctx(ctx); print_attribute(ctx->attr->type, name); free_name(&name); } static void print_ctx(ntfs_attr_search_ctx *ctx) { char *name = get_attr_name_ctx(ctx); print_inode_ni(base_inode(ctx)); print_attribute(ctx->attr->type, name); free_name(&name); } static void print_differ(ntfs_attr *na) { print_na(na); printf("content: DIFFER\n"); } static int cmp_buffer(u8 *buf1, u8 *buf2, long long int size, ntfs_attr *na) { if (memcmp(buf1, buf2, size)) { print_differ(na); return -1; } return 0; } struct cmp_ia { INDEX_ALLOCATION *ia; INDEX_ALLOCATION *tmp_ia; u8 *bitmap; u8 *byte; s64 bm_size; }; static int setup_cmp_ia(ntfs_attr *na, struct cmp_ia *cia) { cia->bitmap = ntfs_attr_readall(na->ni, AT_BITMAP, na->name, na->name_len, &cia->bm_size); if (!cia->bitmap) { perr_println("Failed to readall BITMAP"); return -1; } cia->byte = cia->bitmap; cia->tmp_ia = cia->ia = ntfs_malloc(na->data_size); if (!cia->tmp_ia) goto free_bm; if (ntfs_attr_pread(na, 0, na->data_size, cia->ia) != na->data_size) { perr_println("Failed to pread INDEX_ALLOCATION"); goto free_ia; } return 0; free_ia: free(cia->ia); free_bm: free(cia->bitmap); return -1; } static void cmp_index_allocation(ntfs_attr *na1, ntfs_attr *na2) { struct cmp_ia cia1, cia2; int bit, ret1, ret2; u32 ib_size; if (setup_cmp_ia(na1, &cia1)) return; if (setup_cmp_ia(na2, &cia2)) return; /* * FIXME: ia can be the same even if the bitmap sizes are different. */ if (cia1.bm_size != cia1.bm_size) goto out; if (cmp_buffer(cia1.bitmap, cia2.bitmap, cia1.bm_size, na1)) goto out; if (cmp_buffer((u8 *)cia1.ia, (u8 *)cia2.ia, 0x18, na1)) goto out; ib_size = le32_to_cpu(cia1.ia->index.allocated_size) + 0x18; bit = 0; while ((u8 *)cia1.tmp_ia < (u8 *)cia1.ia + na1->data_size) { if (*cia1.byte & (1 << bit)) { ret1 = ntfs_mst_post_read_fixup((NTFS_RECORD *) cia1.tmp_ia, ib_size); ret2 = ntfs_mst_post_read_fixup((NTFS_RECORD *) cia2.tmp_ia, ib_size); if (ret1 != ret2) { print_differ(na1); goto out; } if (ret1 == -1) continue; if (cmp_buffer(((u8 *)cia1.tmp_ia) + 0x18, ((u8 *)cia2.tmp_ia) + 0x18, le32_to_cpu(cia1.ia-> index.index_length), na1)) goto out; } cia1.tmp_ia = (INDEX_ALLOCATION *)((u8 *)cia1.tmp_ia + ib_size); cia2.tmp_ia = (INDEX_ALLOCATION *)((u8 *)cia2.tmp_ia + ib_size); bit++; if (bit > 7) { bit = 0; cia1.byte++; } } out: free(cia1.ia); free(cia2.ia); free(cia1.bitmap); free(cia2.bitmap); return; } static void cmp_attribute_data(ntfs_attr *na1, ntfs_attr *na2) { s64 pos; s64 count1 = 0, count2; u8 buf1[NTFS_BUF_SIZE]; u8 buf2[NTFS_BUF_SIZE]; for (pos = 0; pos <= na1->data_size; pos += count1) { count1 = ntfs_attr_pread(na1, pos, NTFS_BUF_SIZE, buf1); count2 = ntfs_attr_pread(na2, pos, NTFS_BUF_SIZE, buf2); if (count1 != count2) { print_na(na1); printf("abrupt length: %lld != %lld ", (long long)na1->data_size, (long long)na2->data_size); printf("(count: %lld != %lld)", (long long)count1, (long long)count2); puts(""); return; } if (count1 == -1) { err_printf("%s read error: ", __FUNCTION__); print_na(na1); printf("len = %lld, pos = %lld\n", (long long)na1->data_size, (long long)pos); exit(1); } if (count1 == 0) { if (pos + count1 == na1->data_size) return; /* we are ready */ err_printf("%s read error before EOF: ", __FUNCTION__); print_na(na1); printf("%lld != %lld\n", (long long)pos + count1, (long long)na1->data_size); exit(1); } if (cmp_buffer(buf1, buf2, count1, na1)) return; } err_printf("%s read overrun: ", __FUNCTION__); print_na(na1); err_printf("(len = %lld, pos = %lld, count = %lld)\n", (long long)na1->data_size, (long long)pos, (long long)count1); exit(1); } static int cmp_attribute_header(ATTR_RECORD *a1, ATTR_RECORD *a2) { u32 header_size = offsetof(ATTR_RECORD, resident_end); if (a1->non_resident != a2->non_resident) return 1; if (a1->non_resident) { /* * FIXME: includes paddings which are not handled by ntfsinfo! */ header_size = le32_to_cpu(a1->length); } return memcmp(a1, a2, header_size); } static void cmp_attribute(ntfs_attr_search_ctx *ctx1, ntfs_attr_search_ctx *ctx2) { ATTR_RECORD *a1 = ctx1->attr; ATTR_RECORD *a2 = ctx2->attr; ntfs_attr *na1, *na2; if (cmp_attribute_header(a1, a2)) { print_ctx(ctx1); printf("header: DIFFER\n"); } na1 = ntfs_attr_open(base_inode(ctx1), a1->type, GET_ATTR_NAME(a1)); na2 = ntfs_attr_open(base_inode(ctx2), a2->type, GET_ATTR_NAME(a2)); if ((!na1 && na2) || (na1 && !na2)) { print_ctx(ctx1); printf("open: %s != %s\n", pret2str(na1), pret2str(na2)); goto close_attribs; } if (na1 == NULL) goto close_attribs; if (na1->data_size != na2->data_size) { print_na(na1); printf("length: %lld != %lld\n", (long long)na1->data_size, (long long)na2->data_size); goto close_attribs; } if (ntfs_inode_badclus_bad(inumber(ctx1->ntfs_ino), ctx1->attr) == 1) { /* * If difference exists then it's already reported at the * attribute header since the mapping pairs must differ. */ goto close_attribs; } if (na1->type == AT_INDEX_ALLOCATION) cmp_index_allocation(na1, na2); else cmp_attribute_data(na1, na2); close_attribs: ntfs_attr_close(na1); ntfs_attr_close(na2); } static void vprint_attribute(ATTR_TYPES atype, char *name) { if (!opt.verbose) return; printf("0x%x", atype); if (name) printf(":%s", name); printf(" "); } static void print_attributes(ntfs_inode *ni, ATTR_TYPES atype1, ATTR_TYPES atype2, char *name1, char *name2) { if (!opt.verbose) return; printf("Walking inode %llu attributes: ", (unsigned long long)inumber(ni)); vprint_attribute(atype1, name1); vprint_attribute(atype2, name2); printf("\n"); } static int new_name(ntfs_attr_search_ctx *ctx, char *prev_name) { int ret = 0; char *name = get_attr_name_ctx(ctx); if (prev_name && name) { if (strcmp(prev_name, name) != 0) ret = 1; } else if (prev_name || name) ret = 1; free_name(&name); return ret; } static int new_attribute(ntfs_attr_search_ctx *ctx, ATTR_TYPES prev_atype, char *prev_name) { if (!prev_atype && !prev_name) return 1; if (!ctx->attr->non_resident) return 1; if (prev_atype != ctx->attr->type) return 1; if (new_name(ctx, prev_name)) return 1; if (opt.verbose) { print_inode(base_inode(ctx)->mft_no); print_attribute_ctx(ctx); printf("record %llu lowest_vcn %lld: SKIPPED\n", (unsigned long long)ctx->ntfs_ino->mft_no, (long long)ctx->attr->lowest_vcn); } return 0; } static void set_prev(char **prev_name, ATTR_TYPES *prev_atype, char *name, ATTR_TYPES atype) { free_name(prev_name); if (name) { *prev_name = strdup(name); if (!*prev_name) perr_exit("strdup error"); } *prev_atype = atype; } static void set_cmp_attr(ntfs_attr_search_ctx *ctx, ATTR_TYPES *atype, char **name) { *atype = ctx->attr->type; free_name(name); *name = get_attr_name_ctx(ctx); } static int next_attr(ntfs_attr_search_ctx *ctx, ATTR_TYPES *atype, char **name, int *err) { int ret; ret = ntfs_attrs_walk(ctx); *err = errno; if (ret) { *atype = AT_END; free_name(name); } else set_cmp_attr(ctx, atype, name); return ret; } static int cmp_attributes(ntfs_inode *ni1, ntfs_inode *ni2) { int ret = -1; int old_ret1, ret1 = 0, ret2 = 0; int errno1 = 0, errno2 = 0; char *prev_name = NULL, *name1 = NULL, *name2 = NULL; ATTR_TYPES old_atype1, prev_atype = 0, atype1, atype2; ntfs_attr_search_ctx *ctx1, *ctx2; if (!(ctx1 = attr_get_search_ctx(ni1))) return -1; if (!(ctx2 = attr_get_search_ctx(ni2))) goto out; set_cmp_attr(ctx1, &atype1, &name1); set_cmp_attr(ctx2, &atype2, &name2); while (1) { old_atype1 = atype1; old_ret1 = ret1; if (!ret1 && (le32_to_cpu(atype1) <= le32_to_cpu(atype2) || ret2)) ret1 = next_attr(ctx1, &atype1, &name1, &errno1); if (!ret2 && (le32_to_cpu(old_atype1) >= le32_to_cpu(atype2) || old_ret1)) ret2 = next_attr(ctx2, &atype2, &name2, &errno2); print_attributes(ni1, atype1, atype2, name1, name2); if (ret1 && ret2) { if (errno1 != errno2) { print_inode_ni(ni1); printf("attribute walk (errno): %d != %d\n", errno1, errno2); } break; } if (ret2 || le32_to_cpu(atype1) < le32_to_cpu(atype2)) { if (new_attribute(ctx1, prev_atype, prev_name)) { print_ctx(ctx1); printf("presence: EXISTS != MISSING\n"); set_prev(&prev_name, &prev_atype, name1, atype1); } } else if (ret1 || le32_to_cpu(atype1) > le32_to_cpu(atype2)) { if (new_attribute(ctx2, prev_atype, prev_name)) { print_ctx(ctx2); printf("presence: MISSING != EXISTS \n"); set_prev(&prev_name, &prev_atype, name2, atype2); } } else /* atype1 == atype2 */ { if (new_attribute(ctx1, prev_atype, prev_name)) { cmp_attribute(ctx1, ctx2); set_prev(&prev_name, &prev_atype, name1, atype1); } } } free_name(&prev_name); ret = 0; ntfs_attr_put_search_ctx(ctx2); out: ntfs_attr_put_search_ctx(ctx1); return ret; } static int cmp_inodes(ntfs_volume *vol1, ntfs_volume *vol2) { u64 inode; int ret1, ret2; ntfs_inode *ni1, *ni2; struct progress_bar progress; int pb_flags = 0; /* progress bar flags */ u64 nr_mft_records, nr_mft_records2; if (opt.show_progress) pb_flags |= NTFS_PROGBAR; nr_mft_records = get_nr_mft_records(vol1); nr_mft_records2 = get_nr_mft_records(vol2); if (nr_mft_records != nr_mft_records2) { printf("Number of mft records: %lld != %lld\n", (long long)nr_mft_records, (long long)nr_mft_records2); if (nr_mft_records > nr_mft_records2) nr_mft_records = nr_mft_records2; } progress_init(&progress, 0, nr_mft_records - 1, pb_flags); progress_update(&progress, 0); for (inode = 0; inode < nr_mft_records; inode++) { ret1 = inode_open(vol1, (MFT_REF)inode, &ni1); ret2 = inode_open(vol2, (MFT_REF)inode, &ni2); if (ret1 != ret2) { print_inode(inode); printf("open: %s != %s\n", err2string(ret1), err2string(ret2)); goto close_inodes; } if (ret1 != NTFSCMP_OK) goto close_inodes; if (cmp_attributes(ni1, ni2) != 0) { inode_close(ni1); inode_close(ni2); return -1; } close_inodes: if (inode_close(ni1) != 0) return -1; if (inode_close(ni2) != 0) return -1; progress_update(&progress, inode); } return 0; } static ntfs_volume *mount_volume(const char *volume) { unsigned long mntflag; ntfs_volume *vol = NULL; if (ntfs_check_if_mounted(volume, &mntflag)) { perr_println("Failed to check '%s' mount state", volume); printf("Probably /etc/mtab is missing. It's too risky to " "continue. You might try\nan another Linux distro.\n"); exit(1); } if (mntflag & NTFS_MF_MOUNTED) { if (!(mntflag & NTFS_MF_READONLY)) err_exit("Device '%s' is mounted read-write. " "You must 'umount' it first.\n", volume); } vol = ntfs_mount(volume, NTFS_MNT_RDONLY); if (vol == NULL) { int err = errno; perr_println("Opening '%s' as NTFS failed", volume); if (err == EINVAL) printf(invalid_ntfs_msg, volume); else if (err == EIO) puts(corrupt_volume_msg); else if (err == EPERM) puts(hibernated_volume_msg); exit(1); } return vol; } int main(int argc, char **argv) { ntfs_volume *vol1; ntfs_volume *vol2; printf("%s v%s (libntfs-3g)\n", EXEC_NAME, VERSION); parse_options(argc, argv); utils_set_locale(); vol1 = mount_volume(opt.vol1); vol2 = mount_volume(opt.vol2); if (cmp_inodes(vol1, vol2) != 0) exit(1); ntfs_umount(vol1, FALSE); ntfs_umount(vol2, FALSE); return (0); }