From: Eric Biggers Date: Sun, 19 Aug 2012 17:11:44 +0000 (-0500) Subject: Various fixes X-Git-Tag: v1.0.0~132 X-Git-Url: https://wimlib.net/git/?p=wimlib;a=commitdiff_plain;h=004c7fd634328ab9b3c4b479097a10ca12ae38d7 Various fixes --- diff --git a/src/dentry.c b/src/dentry.c index 92828bb4..76ec2dec 100644 --- a/src/dentry.c +++ b/src/dentry.c @@ -93,10 +93,17 @@ void dentry_to_stbuf(const struct dentry *dentry, struct stat *stbuf, stbuf->st_mode = S_IFREG | 0644; /* Use the size of the unnamed (default) file stream. */ - if (table && (lte = __lookup_resource(table, dentry_hash(dentry)))) - stbuf->st_size = lte->resource_entry.original_size; - else + if (table && (lte = __lookup_resource(table, dentry_hash(dentry)))) { + if (lte->staging_file_name) { + struct stat native_stat; + stat(lte->staging_file_name, &native_stat); + stbuf->st_size = native_stat.st_size; + } else { + stbuf->st_size = lte->resource_entry.original_size; + } + } else { stbuf->st_size = 0; + } stbuf->st_nlink = dentry_link_group_size(dentry); stbuf->st_ino = dentry->hard_link; @@ -161,7 +168,7 @@ void dentry_remove_ads(struct dentry *dentry, struct ads_entry *sentry) { destroy_ads_entry(sentry); memcpy(sentry, sentry + 1, - (dentry->num_ads - (sentry - dentry->ads_entries)) + (dentry->num_ads - (sentry - dentry->ads_entries + 1)) * sizeof(struct ads_entry)); dentry->num_ads--; } @@ -474,6 +481,7 @@ static inline void dentry_common_init(struct dentry *dentry) memset(dentry, 0, sizeof(struct dentry)); dentry->refcnt = 1; dentry->security_id = -1; + dentry->link_group_master_status = GROUP_SLAVE; } /* @@ -515,17 +523,90 @@ void dentry_free_ads_entries(struct dentry *dentry) dentry->num_ads = 0; } - -void free_dentry(struct dentry *dentry) +static void __destroy_dentry(struct dentry *dentry) { FREE(dentry->file_name); FREE(dentry->file_name_utf8); FREE(dentry->short_name); FREE(dentry->full_path_utf8); - dentry_free_ads_entries(dentry); +} + +void free_dentry(struct dentry *dentry) +{ + __destroy_dentry(dentry); + if (dentry->link_group_master_status != GROUP_SLAVE) + dentry_free_ads_entries(dentry); FREE(dentry); } +void put_dentry(struct dentry *dentry) +{ + if (dentry->link_group_master_status == GROUP_MASTER) { + struct dentry *new_master; + list_for_each_entry(new_master, &dentry->link_group_list, + link_group_list) + { + if (new_master->link_group_master_status == GROUP_SLAVE) { + new_master->link_group_master_status = GROUP_MASTER; + dentry->link_group_master_status = GROUP_SLAVE; + break; + } + } + } + list_del(&dentry->link_group_list); + free_dentry(dentry); +} + +static bool dentries_have_same_ads(const struct dentry *d1, + const struct dentry *d2) +{ + /* Verify stream names and hashes are the same */ + for (u16 i = 0; i < d1->num_ads; i++) { + if (strcmp(d1->ads_entries[i].stream_name_utf8, + d2->ads_entries[i].stream_name_utf8) != 0) + return false; + if (memcmp(d1->ads_entries[i].hash, + d2->ads_entries[i].hash, + WIM_HASH_SIZE) != 0) + return false; + } + return true; +} + +/* Share the alternate stream entries between hard-linked dentries. */ +int share_dentry_ads(struct dentry *master, struct dentry *slave) +{ + const char *mismatch_type; + wimlib_assert(master->num_ads == 0 || + master->ads_entries != slave->ads_entries); + if (master->attributes != slave->attributes) { + mismatch_type = "attributes"; + goto mismatch; + } + if (master->security_id != slave->security_id) { + mismatch_type = "security ID"; + goto mismatch; + } + if (memcmp(master->hash, slave->hash, WIM_HASH_SIZE) != 0) { + mismatch_type = "main file resource"; + goto mismatch; + } + if (!dentries_have_same_ads(master, slave)) { + mismatch_type = "Alternate Stream Entries"; + goto mismatch; + } + dentry_free_ads_entries(slave); + slave->ads_entries = master->ads_entries; + slave->link_group_master_status = GROUP_SLAVE; + return 0; +mismatch: + ERROR("Dentries `%s' and `%s' in the same hard-link group but " + "do not share the same %s", + master->full_path_utf8, slave->full_path_utf8, + mismatch_type); + return WIMLIB_ERR_INVALID_DENTRY; +} + /* clones a dentry. * * Beware: @@ -622,7 +703,13 @@ void link_dentry(struct dentry *dentry, struct dentry *parent) } } -/* Unlink a dentry from the directory tree. */ + +/* Unlink a dentry from the directory tree. + * + * Note: This merely removes it from the in-memory tree structure. See + * remove_dentry() in mount.c for a function implemented on top of this one that + * frees the dentry and implements reference counting for the lookup table + * entries. */ void unlink_dentry(struct dentry *dentry) { if (dentry_is_root(dentry)) @@ -648,36 +735,33 @@ static inline void recalculate_dentry_size(struct dentry *dentry) dentry->length = (dentry->length + 7) & ~7; } -static int do_name_change(char **file_name_ret, - char **file_name_utf8_ret, - u16 *file_name_len_ret, - u16 *file_name_utf8_len_ret, - const char *new_name) +int get_names(char **name_utf16_ret, char **name_utf8_ret, + u16 *name_utf16_len_ret, u16 *name_utf8_len_ret, + const char *name) { size_t utf8_len; size_t utf16_len; - char *file_name, *file_name_utf8; + char *name_utf16, *name_utf8; - utf8_len = strlen(new_name); + utf8_len = strlen(name); - file_name = utf8_to_utf16(new_name, utf8_len, &utf16_len); + name_utf8 = utf8_to_utf16(name, utf8_len, &utf16_len); - if (!file_name) + if (!name_utf8) return WIMLIB_ERR_NOMEM; - file_name_utf8 = MALLOC(utf8_len + 1); - if (!file_name_utf8) { - FREE(file_name); + name_utf8 = MALLOC(utf8_len + 1); + if (!name_utf8) { + FREE(name_utf8); return WIMLIB_ERR_NOMEM; } - memcpy(file_name_utf8, new_name, utf8_len + 1); - - FREE(*file_name_ret); - FREE(*file_name_utf8_ret); - *file_name_ret = file_name; - *file_name_utf8_ret = file_name_utf8; - *file_name_len_ret = utf16_len; - *file_name_utf8_len_ret = utf8_len; + memcpy(name_utf8, name, utf8_len + 1); + FREE(*name_utf8_ret); + FREE(*name_utf16_ret); + *name_utf8_ret = name_utf8; + *name_utf16_ret = name_utf16; + *name_utf8_len_ret = utf8_len; + *name_utf16_len_ret = utf16_len; return 0; } @@ -688,9 +772,9 @@ int change_dentry_name(struct dentry *dentry, const char *new_name) { int ret; - ret = do_name_change(&dentry->file_name, &dentry->file_name_utf8, - &dentry->file_name_len, &dentry->file_name_utf8_len, - new_name); + ret = get_names(&dentry->file_name, &dentry->file_name_utf8, + &dentry->file_name_len, &dentry->file_name_utf8_len, + new_name); if (ret == 0) recalculate_dentry_size(dentry); return ret; @@ -698,10 +782,10 @@ int change_dentry_name(struct dentry *dentry, const char *new_name) int change_ads_name(struct ads_entry *entry, const char *new_name) { - return do_name_change(&entry->stream_name, &entry->stream_name_utf8, - &entry->stream_name_len, - &entry->stream_name_utf8_len, - new_name); + return get_names(&entry->stream_name, &entry->stream_name_utf8, + &entry->stream_name_len, + &entry->stream_name_utf8_len, + new_name); } /* Parameters for calculate_dentry_statistics(). */ diff --git a/src/dentry.h b/src/dentry.h index 251e86f6..b14a2eef 100644 --- a/src/dentry.h +++ b/src/dentry.h @@ -192,11 +192,22 @@ struct dentry { /* Alternate stream entries for this dentry. */ struct ads_entry *ads_entries; - /* Number of references to the dentry tree itself, as in multiple - * WIMStructs */ - int refcnt; + union { + /* Number of references to the dentry tree itself, as in multiple + * WIMStructs */ + int refcnt; + + /* Number of times this dentry has been opened (only for + * directories!) */ + u32 num_times_opened; + }; /* List of dentries in the hard link set */ + enum { + GROUP_INDEPENDENT, + GROUP_MASTER, + GROUP_SLAVE + } link_group_master_status; struct list_head link_group_list; }; @@ -251,6 +262,9 @@ extern int for_dentry_in_tree_depth(struct dentry *root, extern int calculate_dentry_full_path(struct dentry *dentry, void *ignore); extern void calculate_subdir_offsets(struct dentry *dentry, u64 *subdir_offset_p); +extern int get_names(char **name_utf16_ret, char **name_utf8_ret, + u16 *name_utf16_len_ret, u16 *name_utf8_len_ret, + const char *name); extern int change_dentry_name(struct dentry *dentry, const char *new_name); extern int change_ads_name(struct ads_entry *entry, const char *new_name); @@ -270,6 +284,9 @@ extern struct dentry *new_dentry(const char *name); extern void dentry_free_ads_entries(struct dentry *dentry); extern void free_dentry(struct dentry *dentry); +extern void put_dentry(struct dentry *dentry); +extern int share_dentry_streams(struct dentry *master, + struct dentry *slave); extern struct dentry *clone_dentry(struct dentry *old); extern void free_dentry_tree(struct dentry *root, struct lookup_table *lookup_table, diff --git a/src/hardlink.c b/src/hardlink.c index 81eb10d4..9a128d46 100644 --- a/src/hardlink.c +++ b/src/hardlink.c @@ -141,13 +141,35 @@ u64 assign_link_groups(struct link_group_table *table) return id; } -#if 0 -/* Load a dentry tree into the link group table */ -int load_link_groups(struct link_group_table *table, struct dentry *root) +static int link_group_free_duplicate_data(struct link_group *group) { - int ret = for_dentry_in_tree(dentry, link_group_table_insert, table); - if (ret == 0) - assign_link_groups(table); - return ret; + struct list_head *head; + struct dentry *master; + + head = group->dentry_list; + master = container_of(head, struct dentry, link_group_list); + head = head->next; + master->link_group_master_status = GROUP_MASTER; + while (head != group->dentry_list) { + int ret = share_dentry_ads(master, + container_of(head, struct dentry, + link_group_list)); + if (ret != 0) + return ret; + } + return 0; +} + +int link_groups_free_duplicate_data(struct link_group_table *table) +{ + for (u64 i = 0; i < table->capacity; i++) { + struct link_group *group = table->array[i]; + while (group) { + int ret = link_group_free_duplicate_data(group); + if (ret != 0) + return ret; + group = group->next; + } + } + return 0; } -#endif diff --git a/src/lookup_table.c b/src/lookup_table.c index 9693cc1b..4df37ea2 100644 --- a/src/lookup_table.c +++ b/src/lookup_table.c @@ -118,7 +118,8 @@ void lookup_table_unlink(struct lookup_table *table, /* Decrement the reference count for the dentry having hash value @hash in the * lookup table. The lookup table entry is unlinked and freed if there are no * references to in remaining. */ -bool lookup_table_decrement_refcnt(struct lookup_table* table, const u8 hash[]) +struct lookup_table_entry * +lookup_table_decrement_refcnt(struct lookup_table* table, const u8 hash[]) { size_t pos = *(size_t*)hash % table->capacity; struct lookup_table_entry *prev = NULL; @@ -129,19 +130,21 @@ bool lookup_table_decrement_refcnt(struct lookup_table* table, const u8 hash[]) if (memcmp(hash, entry->hash, WIM_HASH_SIZE) == 0) { wimlib_assert(entry->refcnt != 0); if (--entry->refcnt == 0) { - if (entry->num_opened_fds == 0) + if (entry->num_opened_fds == 0) { free_lookup_table_entry(entry); + entry = NULL; + } if (prev) prev->next = next; else table->array[pos] = next; - return true; + break; } } prev = entry; entry = next; } - return false; + return entry; } diff --git a/src/lookup_table.h b/src/lookup_table.h index 423af6dc..434b9d89 100644 --- a/src/lookup_table.h +++ b/src/lookup_table.h @@ -115,8 +115,8 @@ extern void lookup_table_insert(struct lookup_table *table, extern void lookup_table_unlink(struct lookup_table *table, struct lookup_table_entry *lte); -extern bool lookup_table_decrement_refcnt(struct lookup_table* table, - const u8 hash[]); +extern struct lookup_table_entry * +lookup_table_decrement_refcnt(struct lookup_table* table, const u8 hash[]); extern struct lookup_table_entry *new_lookup_table_entry(); diff --git a/src/mount.c b/src/mount.c index 0588746e..b8fe5278 100644 --- a/src/mount.c +++ b/src/mount.c @@ -146,6 +146,45 @@ static int close_wimlib_fd(struct wimlib_fd *fd) return 0; } +static void remove_dentry(struct dentry *dentry, + struct lookup_table *lookup_table) +{ + const u8 *hash = dentry->hash; + u16 i = 0; + struct lookup_table_entry *lte; + while (1) { + lte = lookup_table_decrement_refcnt(lookup_table, hash); + if (lte && lte->num_opened_fds) + for (u16 i = 0; i < lte->num_allocated_fds; i++) + if (lte->fds[i] && lte->fds[i]->dentry == dentry) + lte->fds[i]->dentry = NULL; + if (i == dentry->num_ads) + break; + hash = dentry->ads_entries[i].hash; + i++; + } + + unlink_dentry(dentry); + put_dentry(dentry); +} + +static void dentry_increment_lookup_table_refcnts(struct dentry *dentry, + struct lookup_table *lookup_table) +{ + u16 i = 0; + const u8 *hash = dentry->hash; + struct lookup_table_entry *lte; + while (1) { + lte = __lookup_resource(lookup_table, hash); + if (lte) + lte->refcnt++; + if (i == dentry->num_ads) + break; + hash = dentry->ads_entries[i].hash; + i++; + } +} + /* Creates a new staging file and returns its file descriptor opened for * writing. * @@ -221,7 +260,7 @@ static int extract_resource_to_staging_dir(struct dentry *dentry, int fd; struct lookup_table_entry *old_lte, *new_lte; size_t link_group_size; - + old_lte = *lte; fd = create_staging_file(&staging_file_name, O_WRONLY); if (fd == -1) @@ -238,9 +277,7 @@ static int extract_resource_to_staging_dir(struct dentry *dentry, else ret = -EIO; close(fd); - unlink(staging_file_name); - FREE(staging_file_name); - return ret; + goto out_delete_staging_file; } link_group_size = dentry_link_group_size(dentry); @@ -266,8 +303,10 @@ static int extract_resource_to_staging_dir(struct dentry *dentry, wimlib_assert(old_lte->refcnt > link_group_size); new_lte = new_lookup_table_entry(); - if (!new_lte) - return -ENOMEM; + if (!new_lte) { + ret = -ENOMEM; + goto out_delete_staging_file; + } u16 num_transferred_fds = 0; for (u16 i = 0; i < old_lte->num_allocated_fds; i++) { @@ -282,7 +321,8 @@ static int extract_resource_to_staging_dir(struct dentry *dentry, sizeof(new_lte->fds[0])); if (!new_lte->fds) { free_lookup_table_entry(new_lte); - return -ENOMEM; + ret = -ENOMEM; + goto out_delete_staging_file; } u16 j = 0; for (u16 i = 0; i < old_lte->num_allocated_fds; i++) { @@ -302,6 +342,12 @@ static int extract_resource_to_staging_dir(struct dentry *dentry, new_lte->num_opened_fds = j; } + } else { + new_lte = new_lookup_table_entry(); + if (!new_lte) { + ret = -ENOMEM; + goto out_delete_staging_file; + } } new_lte->resource_entry.original_size = size; new_lte->refcnt = link_group_size; @@ -311,6 +357,10 @@ static int extract_resource_to_staging_dir(struct dentry *dentry, lookup_table_insert(w->lookup_table, new_lte); *lte = new_lte; return 0; +out_delete_staging_file: + unlink(staging_file_name); + FREE(staging_file_name); + return ret; } /* @@ -777,6 +827,7 @@ static int wimfs_link(const char *to, const char *from) } list_add(&from_dentry->link_group_list, &to_dentry->link_group_list); link_dentry(from_dentry, from_dentry_parent); + dentry_increment_lookup_table_refcnts(from_dentry, w->lookup_table); return 0; } @@ -836,11 +887,14 @@ static int wimfs_mknod(const char *path, mode_t mode, dev_t rdev) return -ENOENT; if (!dentry_is_directory(parent)) return -ENOTDIR; + basename = path_basename(path); if (get_dentry_child_with_name(parent, path)) return -EEXIST; dentry = new_dentry(basename); + if (!dentry) + return -ENOMEM; link_dentry(dentry, parent); } return 0; @@ -872,10 +926,10 @@ static int wimfs_open(const char *path, struct fuse_file_info *fi) return 0; } - ret = extract_resource_to_staging_dir(dentry, <e, - lte->resource_entry.original_size); + ret = extract_resource_to_staging_dir(dentry, <e, 0); if (ret != 0) return ret; + memcpy(dentry_hash, lte->hash, WIM_HASH_SIZE); } ret = alloc_wimlib_fd(lte, &fd); @@ -896,6 +950,7 @@ static int wimfs_open(const char *path, struct fuse_file_info *fi) lte->resource_entry.original_size); if (ret != 0) return ret; + memcpy(dentry_hash, lte->hash, WIM_HASH_SIZE); } if (lte->staging_file_name) { fd->staging_fd = open(lte->staging_file_name, fi->flags); @@ -914,8 +969,11 @@ static int wimfs_opendir(const char *path, struct fuse_file_info *fi) struct dentry *dentry; dentry = get_dentry(w, path); - if (!dentry || !dentry_is_directory(dentry)) + if (!dentry) + return -ENOENT; + if (!dentry_is_directory(dentry)) return -ENOTDIR; + dentry->num_times_opened++; fi->fh = (uint64_t)dentry; return 0; } @@ -942,6 +1000,7 @@ static int wimfs_read(const char *path, char *buf, size_t size, wimlib_assert(fd->staging_fd != -1); ssize_t ret; + DEBUG("Seek to offset %zu", offset); if (lseek(fd->staging_fd, offset, SEEK_SET) == -1) return -errno; @@ -1026,15 +1085,38 @@ static int wimfs_release(const char *path, struct fuse_file_info *fi) wimlib_assert(!(mount_flags & WIMLIB_MOUNT_FLAG_READWRITE)); return 0; } + + if (flags_writable(fi->flags) && fd->dentry) { + u64 now = get_timestamp(); + fd->dentry->last_access_time = now; + fd->dentry->last_write_time = now; + } + return close_wimlib_fd(fd); } +static int wimfs_releasedir(const char *path, struct fuse_file_info *fi) +{ + struct dentry *dentry = (struct dentry *)fi->fh; + + wimlib_assert(dentry->num_times_opened); + if (--dentry->num_times_opened == 0) + free_dentry(dentry); + return 0; +} + /* Renames a file or directory. See rename (3) */ static int wimfs_rename(const char *from, const char *to) { struct dentry *src; struct dentry *dst; struct dentry *parent_of_dst; + char *file_name_utf16 = NULL, *file_name_utf8 = NULL; + u16 file_name_utf16_len, file_name_utf8_len; + int ret; + + /* This rename() implementation currently only supports actual files + * (not alternate data streams) */ src = get_dentry(w, from); if (!src) @@ -1042,7 +1124,17 @@ static int wimfs_rename(const char *from, const char *to) dst = get_dentry(w, to); + + ret = get_names(&file_name_utf16, &file_name_utf8, + &file_name_utf16_len, &file_name_utf8_len, + path_basename(to)); + if (ret != 0) + return -ENOMEM; + if (dst) { + if (src == dst) /* Same file */ + return 0; + if (!dentry_is_directory(src)) { /* Cannot rename non-directory to directory. */ if (dentry_is_directory(dst)) @@ -1056,19 +1148,22 @@ static int wimfs_rename(const char *from, const char *to) return -ENOTEMPTY; } parent_of_dst = dst->parent; - unlink_dentry(dst); - lookup_table_decrement_refcnt(w->lookup_table, dst->hash); - free_dentry(dst); + remove_dentry(dst, w->lookup_table); } else { parent_of_dst = get_parent_dentry(w, to); if (!parent_of_dst) return -ENOENT; } + FREE(src->file_name); + FREE(src->file_name_utf8); + src->file_name = file_name_utf16; + src->file_name_utf8 = file_name_utf8; + src->file_name_len = file_name_utf16_len; + src->file_name_utf8_len = file_name_utf8_len; + unlink_dentry(src); - change_dentry_name(src, path_basename(to)); link_dentry(src, parent_of_dst); - /*calculate_dentry_full_path(src);*/ return 0; } @@ -1085,7 +1180,8 @@ static int wimfs_rmdir(const char *path) return -ENOTEMPTY; unlink_dentry(dentry); - free_dentry(dentry); + if (dentry->num_times_opened == 0) + free_dentry(dentry); return 0; } @@ -1174,18 +1270,7 @@ static int wimfs_unlink(const char *path) if (dentry_hash == dentry->hash) { /* We are removing the full dentry including all alternate data * streams. */ - const u8 *hash = dentry->hash; - u16 i = 0; - while (1) { - lookup_table_decrement_refcnt(w->lookup_table, hash); - if (i == dentry->num_ads) - break; - hash = dentry->ads_entries[i].hash; - i++; - } - - unlink_dentry(dentry); - free_dentry(dentry); + remove_dentry(dentry, w->lookup_table); } else { /* We are removing an alternate data stream. */ struct ads_entry *cur_entry = dentry->ads_entries; @@ -1236,36 +1321,33 @@ static int wimfs_write(const char *path, const char *buf, size_t size, if (ret == -1) return -errno; - /* The file has been modified, so all its timestamps must be - * updated. */ - dentry_update_all_timestamps(fd->dentry); - return ret; } static struct fuse_operations wimfs_operations = { - .access = wimfs_access, - .destroy = wimfs_destroy, - .fgetattr = wimfs_fgetattr, - .ftruncate = wimfs_ftruncate, - .getattr = wimfs_getattr, - .link = wimfs_link, - .mkdir = wimfs_mkdir, - .mknod = wimfs_mknod, - .open = wimfs_open, - .opendir = wimfs_opendir, - .read = wimfs_read, - .readdir = wimfs_readdir, - .readlink = wimfs_readlink, - .release = wimfs_release, - .rename = wimfs_rename, - .rmdir = wimfs_rmdir, - .symlink = wimfs_symlink, - .truncate = wimfs_truncate, - .unlink = wimfs_unlink, - .utimens = wimfs_utimens, - .write = wimfs_write, + .access = wimfs_access, + .destroy = wimfs_destroy, + .fgetattr = wimfs_fgetattr, + .ftruncate = wimfs_ftruncate, + .getattr = wimfs_getattr, + .link = wimfs_link, + .mkdir = wimfs_mkdir, + .mknod = wimfs_mknod, + .open = wimfs_open, + .opendir = wimfs_opendir, + .read = wimfs_read, + .readdir = wimfs_readdir, + .readlink = wimfs_readlink, + .release = wimfs_release, + .releasedir = wimfs_releasedir, + .rename = wimfs_rename, + .rmdir = wimfs_rmdir, + .symlink = wimfs_symlink, + .truncate = wimfs_truncate, + .unlink = wimfs_unlink, + .utimens = wimfs_utimens, + .write = wimfs_write, }; diff --git a/src/resource.c b/src/resource.c index 731bd1c0..4b71cb3f 100644 --- a/src/resource.c +++ b/src/resource.c @@ -1008,6 +1008,10 @@ int read_metadata_resource(FILE *fp, int wim_ctype, struct image_metadata *imd) if (ret != 0) goto out_free_lgt; + ret = link_groups_free_duplicate_data(lgt); + if (ret != 0) + goto out_free_lgt; + imd->lgt = lgt; imd->security_data = sd; imd->root_dentry = dentry; diff --git a/src/wimlib_internal.h b/src/wimlib_internal.h index d9c77756..68ec73f5 100644 --- a/src/wimlib_internal.h +++ b/src/wimlib_internal.h @@ -333,6 +333,7 @@ int link_group_table_insert(struct dentry *dentry, struct link_group_table *table); void free_link_group_table(struct link_group_table *table); u64 assign_link_groups(struct link_group_table *table); +int link_groups_free_duplicate_data(struct link_group_table *table); /* header.c */