Пример #1
0
/* This does preprocessing including handling of "other lint" (non-dupes)
 * After rm_preprocess(), all remaining duplicate candidates are in
 * a jagged GSList of GSLists as follows:
 * session->tables->size_groups->group1->file1a
 *                                     ->file1b
 *                                     ->file1c
 *                             ->group2->file2a
 *                                     ->file2b
 *                                       etc
 */
void rm_preprocess(RmSession *session) {
    RmFileTables *tables = session->tables;
    GQueue *all_files = tables->all_files;

    session->total_filtered_files = session->total_files;

    /* initial sort by size */
    g_queue_sort(all_files, (GCompareDataFunc)rm_file_cmp_full, session);
    rm_log_debug_line("initial size sort finished at time %.3f; sorted %d files",
                      g_timer_elapsed(session->timer, NULL),
                      session->total_files);

    /* split into file size groups; for each size, remove path doubles and bundle
     * hardlinks */
    rm_assert_gentle(all_files->head);
    RmFile *file = g_queue_pop_head(all_files);
    RmFile *current_size_file = file;
    guint removed = 0;
    GHashTable *node_table = tables->node_table;
    while(file && !rm_session_was_aborted()) {
        /* group files into inode clusters */
        GQueue *inode_cluster =
            rm_hash_table_setdefault(node_table, file, (RmNewFunc)g_queue_new);

        g_queue_push_tail(inode_cluster, file);

        /* get next file and check if it is part of the same group */
        file = g_queue_pop_head(all_files);
        if(!file || rm_file_cmp(file, current_size_file) != 0) {
            /* process completed group (all same size & other criteria)*/
            /* remove path doubles and handle "other" lint */

            /* add an empty GSlist to our list of lists */
            tables->size_groups = g_slist_prepend(tables->size_groups, NULL);

            removed += g_hash_table_foreach_remove(
                node_table, (GHRFunc)rm_pp_handle_inode_clusters, session);

            /* free up the node table for the next group */
            g_hash_table_steal_all(node_table);
            if(tables->size_groups->data == NULL) {
                /* zero size group after handling other lint; remove it */
                tables->size_groups = g_slist_delete_link(tables->size_groups, tables->size_groups);
            }
            current_size_file = file;
        }
    }

    session->other_lint_cnt += rm_pp_handler_other_lint(session);

    rm_log_debug_line(
        "path doubles removal/hardlink bundling/other lint finished at %.3f; removed %u "
        "of %d",
        g_timer_elapsed(session->timer, NULL), removed, session->total_files);

    rm_fmt_set_state(session->formats, RM_PROGRESS_STATE_PREPROCESS);
}
Пример #2
0
dev_t rm_mounts_get_disk_id(RmMountTable *self, dev_t partition, const char *path) {
    if(self == NULL) {
        return 0;
    }

#if RM_MOUNTTABLE_IS_USABLE

    RmPartitionInfo *part =
        g_hash_table_lookup(self->part_table, GINT_TO_POINTER(partition));
    if(part) {
        return part->disk;
    } else {
        /* probably a btrfs subvolume which is not a mountpoint; walk up tree until we get
         * to *
         * a recognisable partition */
        char *prev = g_strdup(path);
        while(TRUE) {
            char *temp = g_strdup(prev);
            char *parent_path = g_strdup(dirname(temp));
            g_free(temp);

            RmStat stat_buf;
            if(!rm_sys_stat(parent_path, &stat_buf)) {
                RmPartitionInfo *parent_part = g_hash_table_lookup(
                    self->part_table, GINT_TO_POINTER(stat_buf.st_dev));
                if(parent_part) {
                    /* create new partition table entry */
                    rm_log_debug_line("Adding partition info for " GREEN "%s" RESET
                                      " - looks like subvolume %s on disk " GREEN
                                      "%s" RESET,
                                      path, prev, parent_part->name);
                    part = rm_part_info_new(prev, parent_part->fsname, parent_part->disk);
                    g_hash_table_insert(self->part_table, GINT_TO_POINTER(partition),
                                        part);
                    if(g_hash_table_contains(self->reflinkfs_table,
                                             GUINT_TO_POINTER(stat_buf.st_dev))) {
                        g_hash_table_insert(self->reflinkfs_table,
                                            GUINT_TO_POINTER(partition),
                                            GUINT_TO_POINTER(1));
                    }
                    g_free(prev);
                    g_free(parent_path);
                    return parent_part->disk;
                }
            }
            g_free(prev);
            prev = parent_path;
            rm_assert_gentle(strcmp(prev, "/") != 0);
            rm_assert_gentle(strcmp(prev, ".") != 0);
        }
    }
#else
    (void) partition;
    (void) path;
    return 0;
#endif
}
Пример #3
0
static void rm_traverse_session_free(RmTravSession *trav_session) {
    rm_log_debug_line("Found %d files, ignored %d hidden files and %d hidden folders",
                      trav_session->session->total_files,
                      trav_session->session->ignored_files,
                      trav_session->session->ignored_folders);

    rm_userlist_destroy(trav_session->userlist);

    g_free(trav_session);
}
Пример #4
0
void rm_mds_start(RmMDS *mds) {
    guint disk_count = g_hash_table_size(mds->disks);
    guint threads = CLAMP(mds->threads_per_disk * disk_count, 1, (guint)mds->max_threads);
    rm_log_debug_line("Starting MDS scheduler with %i threads", threads);

    mds->pool = rm_util_thread_pool_new((GFunc)rm_mds_factory, mds, threads);
    mds->running = TRUE;
    GList *disks = g_hash_table_get_values(mds->disks);
    g_list_foreach(disks, (GFunc)rm_mds_device_start, mds);
    g_list_free(disks);
}
Пример #5
0
static size_t rm_pp_parse_pattern(const char *pattern, GRegex **regex, GError **error) {
    if(*pattern != '<') {
        g_set_error(error, RM_ERROR_QUARK, 0, _("Pattern has to start with `<`"));
        return 0;
    }

    /* Balance of start and end markers */
    int balance = 1;
    char *iter = (char *)pattern;
    char *last = iter;

    while((iter = strpbrk(&iter[1], "<>"))) {
        if(iter[-1] == '\\') {
            /* escaped, skip */
            break;
        }

        if(iter && *iter == '<') {
            ++balance;
        } else if(iter) {
            --balance;
            last = iter;
        }

        if(balance == 0) {
            break;
        }
    }

    if(balance != 0) {
        g_set_error(error, RM_ERROR_QUARK, 0, _("`<` or `>` imbalance: %d"), balance);
        return 0;
    }

    size_t src_len = (last - pattern - 1);

    if(src_len == 0) {
        g_set_error(error, RM_ERROR_QUARK, 0, _("empty pattern"));
        return 0;
    }

    GString *part = g_string_new_len(&pattern[1], src_len);

    rm_log_debug_line("Compiled pattern: %s\n", part->str);

    /* Actually compile the pattern: */
    *regex = g_regex_new(part->str, G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, error);

    g_string_free(part, TRUE);

    /* Include <> in the result len */
    return src_len + 2;
}
Пример #6
0
/** @brief Push an RmMDSDevice to the threadpool
 **/
void rm_mds_device_start(RmMDSDevice *device, RmMDS *mds) {
    rm_assert_gentle(device->threads == 0);

    device->threads = mds->threads_per_disk;
    g_mutex_lock(&device->lock);
    {
        for(int i = 0; i < mds->threads_per_disk; ++i) {
            rm_log_debug_line("Starting disk %" LLU " (pointer %p) thread #%i",
                              (RmOff)device->disk, device, i + 1);
            rm_util_thread_pool_push(mds->pool, device);
        }
    }
    g_mutex_unlock(&device->lock);
}
Пример #7
0
void rm_buffer_pool_destroy(RmBufferPool *pool) {
    rm_log_debug_line("had %" G_GSIZE_FORMAT " unused read buffers",
                      pool->min_kept_buffers);

    /* Wait for all buffers to come back */
    g_mutex_lock(&pool->lock);
    {
        /* Free 'em */
        g_slist_free_full(pool->stack, (GDestroyNotify)rm_buffer_free);
    }
    g_mutex_unlock(&pool->lock);

    g_mutex_clear(&pool->lock);
    g_cond_clear(&pool->change);
    g_slice_free(RmBufferPool, pool);
}
Пример #8
0
RmBufferPool *rm_buffer_pool_init(gsize buffer_size, gsize max_mem, gsize max_kept_mem) {
    RmBufferPool *self = g_slice_new(RmBufferPool);
    self->stack = NULL;
    self->buffer_size = buffer_size;
    self->avail_buffers = MAX(max_mem / buffer_size, 1);
    self->min_kept_buffers = self->avail_buffers;
    self->max_kept_buffers = MAX(max_kept_mem / buffer_size, 1);
    self->kept_buffers = 0;

    rm_log_debug_line("rm_buffer_pool_init: allocated max %" G_GSIZE_FORMAT
                      " buffers of %" G_GSIZE_FORMAT " bytes each",
                      self->avail_buffers, self->buffer_size);
    g_cond_init(&self->change);
    g_mutex_init(&self->lock);
    return self;
}
Пример #9
0
void rm_session_read_kernel_version(RmSession *session) {
    struct utsname buf;
    if(uname(&buf) == -1) {
        return;
    }

    if(sscanf(buf.release, "%d.%d.*", &session->kernel_version[0],
              &session->kernel_version[1]) == EOF) {
        session->kernel_version[0] = -1;
        session->kernel_version[1] = -1;
        return;
    }

    rm_log_debug_line("Linux kernel version is %d.%d.",
                      session->kernel_version[0],
                      session->kernel_version[1]);
}
Пример #10
0
static void rm_traverse_directory(RmTravBuffer *buffer, RmTravSession *trav_session) {
    RmSession *session = trav_session->session;
    RmCfg *cfg = session->cfg;

    char is_prefd = buffer->is_prefd;
    RmOff path_index = buffer->path_index;

    /* Initialize ftsp */
    int fts_flags = FTS_PHYSICAL | FTS_COMFOLLOW | FTS_NOCHDIR;

    RM_BUFFER_DEFINE_PATH(trav_session->session, buffer);

    bool is_on_subvol_fs = (buffer_path[0] == '/' && buffer_path[1] == '/');
    if(is_on_subvol_fs) {
        rm_log_debug_line("Treating files under %s as a single volume", buffer_path);
    }

    FTS *ftsp = fts_open((char * [2]){buffer_path, NULL}, fts_flags, NULL);
Пример #11
0
void rm_json_cache_parse_entry(_U JsonArray *array, _U guint index,
                               JsonNode *element_node, RmTrie *file_trie) {
    if(JSON_NODE_TYPE(element_node) != JSON_NODE_OBJECT) {
        return;
    }

    JsonObject *object = json_node_get_object(element_node);
    JsonNode *mtime_node = json_object_get_member(object, "mtime");
    JsonNode *path_node = json_object_get_member(object, "path");
    JsonNode *cksum_node = json_object_get_member(object, "checksum");
    JsonNode *type_node = json_object_get_member(object, "type");

    if(mtime_node && path_node && cksum_node && type_node) {
        RmStat stat_buf;
        const char *path = json_node_get_string(path_node);
        const char *cksum = json_node_get_string(cksum_node);
        const char *type = json_node_get_string(type_node);

        if(g_strcmp0(type, "duplicate_file") && g_strcmp0(type, "unfinished_cksum")) {
            /* some other file that has a checksum for weird reasons.
             * This is here to prevent errors like reporting files with
             * empty checksums as duplicates.
             * */
            return;
        }

        if(rm_sys_stat(path, &stat_buf) == -1) {
            /* file does not appear to exist */
            return;
        }

        if(json_node_get_int(mtime_node) < rm_sys_stat_mtime_seconds(&stat_buf)) {
            /* file is newer than stored checksum */
            return;
        }

        char *cksum_copy = g_strdup(cksum);
        if(!rm_trie_set_value(file_trie, path, cksum_copy)) {
            g_free(cksum_copy);
        }
        rm_log_debug_line("* Adding cache entry %s (%s)", path, cksum);
    }
}
Пример #12
0
/* RmMDSDevice */
static RmMDSDevice *rm_mds_device_new(RmMDS *mds, const dev_t disk) {
    RmMDSDevice *self = g_slice_new0(RmMDSDevice);

    g_mutex_init(&self->lock);
    g_cond_init(&self->cond);

    self->mds = mds;
    self->ref_count = 0;
    self->threads = 0;
    self->disk = disk;

    if(mds->fake_disk) {
        self->is_rotational = (disk % 2 == 0);
    } else {
        self->is_rotational = !rm_mounts_is_nonrotational(mds->mount_table, disk);
    }

    rm_log_debug_line("Created new RmMDSDevice for %srotational disk #%" LLU,
                      self->is_rotational ? "" : "non-", (RmOff)disk);
    return self;
}
Пример #13
0
/** @brief RmMDSDevice worker thread
 **/
static void rm_mds_factory(RmMDSDevice *device, RmMDS *mds) {
    /* rm_mds_factory processes tasks from device->task_list.
     * After completing one pass of the device, returns self to the
     * mds->pool threadpool. */
    gint processed = 0;
    g_mutex_lock(&device->lock);
    {
        /* check for empty queues - if so then wait a little while before giving up */
        if(!device->sorted_tasks && !device->unsorted_tasks && device->ref_count > 0) {
            /* timed wait for signal from rm_mds_push_task_impl() */
            gint64 end_time = g_get_monotonic_time() + MDS_EMPTYQUEUE_SLEEP_US;
            g_cond_wait_until(&device->cond, &device->lock, end_time);
        }

        /* sort and merge task lists */
        if(device->unsorted_tasks) {
            if(mds->prioritiser) {
                device->sorted_tasks = g_slist_concat(
                                           g_slist_sort_with_data(device->unsorted_tasks,
                                                   (GCompareDataFunc)rm_mds_compare,
                                                   (RmMDSSortFunc)mds->prioritiser),
                                           device->sorted_tasks);
            } else {
                device->sorted_tasks =
                    g_slist_concat(device->unsorted_tasks, device->sorted_tasks);
            }
            device->unsorted_tasks = NULL;
        }
    }
    g_mutex_unlock(&device->lock);

    /* process tasks from device->sorted_tasks */
    RmMDSTask *task = NULL;
    while(processed < mds->pass_quota && (task = rm_util_slist_pop(&device->sorted_tasks, &device->lock))) {
        if(mds->func(task->task_data, mds->user_data)) {
            /* task succeeded; update counters */
            ++processed;
        }
        rm_mds_task_free(task);
    }

    if(rm_mds_device_ref(device, 0) > 0) {
        /* return self to pool for further processing */
        if(processed == 0) {
            /* stalled queue; chill for a bit */
            g_usleep(1000);
        }
        rm_util_thread_pool_push(mds->pool, device);
    } else if(g_atomic_int_dec_and_test(&device->threads)) {
        /* free self and signal to rm_mds_free() */
        g_mutex_lock(&mds->lock);
        {
            rm_log_debug_line("Freeing device %" LLU " (pointer %p)", (RmOff)device->disk,
                              device);
            g_hash_table_remove(mds->disks, GINT_TO_POINTER(device->disk));
            rm_mds_device_free(device);
            g_cond_signal(&mds->cond);
        }
        g_mutex_unlock(&mds->lock);
    }
}
Пример #14
0
void rm_digest_buffered_update(RmBuffer *buffer) {
    RmDigest *digest = buffer->digest;
    if(digest->type != RM_DIGEST_PARANOID) {
        rm_digest_update(digest, buffer->data, buffer->len);
        rm_buffer_release(buffer);
    } else {
        RmParanoid *paranoid = digest->paranoid;

        /* efficiently append buffer to buffers GSList */
        if(!paranoid->buffers) {
            /* first buffer */
            paranoid->buffers = g_slist_prepend(NULL, buffer);
            paranoid->buffer_tail = paranoid->buffers;
        } else {
            paranoid->buffer_tail = g_slist_append(paranoid->buffer_tail, buffer)->next;
        }

        digest->bytes += buffer->len;

        if(paranoid->shadow_hash) {
            rm_digest_update(paranoid->shadow_hash, buffer->data, buffer->len);
        }

        if(paranoid->twin_candidate) {
            /* do a running check that digest remains the same as its candidate twin */
            if(rm_buffer_equal(buffer, paranoid->twin_candidate_buffer->data)) {
                /* buffers match; move ptr to next one ready for next buffer */
                paranoid->twin_candidate_buffer = paranoid->twin_candidate_buffer->next;
            } else {
                /* buffers don't match - delete candidate (new candidate might be added on
                 * next
                 * call to rm_digest_buffered_update) */
                paranoid->twin_candidate = NULL;
                paranoid->twin_candidate_buffer = NULL;
#if _RM_CHECKSUM_DEBUG
                rm_log_debug_line("Ejected candidate match at buffer #%u",
                                  g_slist_length(paranoid->buffers));
#endif
            }
        }

        while(!paranoid->twin_candidate && paranoid->incoming_twin_candidates &&
              (paranoid->twin_candidate =
                   g_async_queue_try_pop(paranoid->incoming_twin_candidates))) {
            /* validate the new candidate by comparing the previous buffers (not
             * including current)*/
            paranoid->twin_candidate_buffer = paranoid->twin_candidate->paranoid->buffers;
            GSList *iter_self = paranoid->buffers;
            gboolean match = TRUE;
            while(match && iter_self) {
                match = (rm_buffer_equal(paranoid->twin_candidate_buffer->data,
                                         iter_self->data));
                iter_self = iter_self->next;
                paranoid->twin_candidate_buffer = paranoid->twin_candidate_buffer->next;
            }
            if(paranoid->twin_candidate && !match) {
/* reject the twin candidate, also add to rejects list to speed up rm_digest_equal() */
#if _RM_CHECKSUM_DEBUG
                rm_log_debug_line("Rejected twin candidate %p for %p",
                                  paranoid->twin_candidate, paranoid);
#endif
                if(!paranoid->shadow_hash) {
                    /* we use the rejects file to speed up rm_digest_equal */
                    paranoid->rejects =
                        g_slist_prepend(paranoid->rejects, paranoid->twin_candidate);
                }
                paranoid->twin_candidate = NULL;
                paranoid->twin_candidate_buffer = NULL;
#if _RM_CHECKSUM_DEBUG
            } else {
                rm_log_debug_line("Added twin candidate %p for %p",
                                  paranoid->twin_candidate, paranoid);
#endif
            }
        }
    }
}
Пример #15
0
static bool rm_mounts_create_tables(RmMountTable *self, bool force_fiemap) {
    /* partition dev_t to disk dev_t */
    self->part_table = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL,
                                             (GDestroyNotify)rm_part_info_free);

    /* disk dev_t to boolean indication if disk is rotational */
    self->disk_table = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL,
                                             (GDestroyNotify)rm_disk_info_free);

    self->nfs_table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);

    /* Mapping dev_t => true (used as set) */
    self->evilfs_table = g_hash_table_new(NULL, NULL);
    self->reflinkfs_table = g_hash_table_new(NULL, NULL);

    RmMountEntry *entry = NULL;
    RmMountEntries *mnt_entries = rm_mount_list_open(self);

    if(mnt_entries == NULL) {
        return false;
    }

    while((entry = rm_mount_list_next(mnt_entries))) {
        RmStat stat_buf_folder;
        if(rm_sys_stat(entry->dir, &stat_buf_folder) == -1) {
            continue;
        }

        dev_t whole_disk = 0;
        gchar is_rotational = true;
        char diskname[PATH_MAX];
        memset(diskname, 0, sizeof(diskname));

        RmStat stat_buf_dev;
        if(rm_sys_stat(entry->fsname, &stat_buf_dev) == -1) {
            char *nfs_marker = NULL;
            /* folder rm_sys_stat() is ok but devname rm_sys_stat() is not; this happens
             * for example
             * with tmpfs and with nfs mounts.  Try to handle a few such cases.
             * */
            if(rm_mounts_is_ramdisk(entry->fsname)) {
                strncpy(diskname, entry->fsname, sizeof(diskname));
                is_rotational = false;
                whole_disk = stat_buf_folder.st_dev;
            } else if((nfs_marker = strstr(entry->fsname, ":/")) != NULL) {
                size_t until_slash =
                    MIN((int)sizeof(entry->fsname), nfs_marker - entry->fsname);
                strncpy(diskname, entry->fsname, until_slash);
                is_rotational = true;

                /* Assign different dev ids (with major id 0) to different nfs servers */
                if(!g_hash_table_contains(self->nfs_table, diskname)) {
                    g_hash_table_insert(self->nfs_table, g_strdup(diskname), NULL);
                }
                whole_disk = makedev(0, g_hash_table_size(self->nfs_table));
            } else {
                strncpy(diskname, "unknown", sizeof(diskname));
                is_rotational = true;
                whole_disk = 0;
            }
        } else {
            if(rm_mounts_devno_to_wholedisk(entry, stat_buf_dev.st_rdev, diskname,
                                            sizeof(diskname), &whole_disk) == -1) {
                /* folder and devname rm_sys_stat() are ok but blkid failed; this happens
                 * when?
                 * Treat as a non-rotational device using devname dev as whole_disk key
                 * */
                rm_log_debug_line(RED "devno_to_wholedisk failed for %s" RESET,
                                  entry->fsname);
                whole_disk = stat_buf_dev.st_dev;
                strncpy(diskname, entry->fsname, sizeof(diskname));
                is_rotational = false;
            } else {
                is_rotational = rm_mounts_is_rotational_blockdev(diskname);
            }
        }

        is_rotational |= force_fiemap;

        RmPartitionInfo *existing = g_hash_table_lookup(
            self->part_table, GUINT_TO_POINTER(stat_buf_folder.st_dev));
        if(!existing || (existing->disk == 0 && whole_disk != 0)) {
            if(existing) {
                rm_log_debug_line("Replacing part_table entry %s for path %s with %s",
                                  existing->fsname, entry->dir, entry->fsname);
            }
            g_hash_table_insert(self->part_table,
                                GUINT_TO_POINTER(stat_buf_folder.st_dev),
                                rm_part_info_new(entry->dir, entry->fsname, whole_disk));
        } else {
            rm_log_debug_line("Skipping duplicate mount entry for dir %s dev %02u:%02u",
                              entry->dir, major(stat_buf_folder.st_dev),
                              minor(stat_buf_folder.st_dev));
            continue;
        }

        /* small hack, so also the full disk id can be given to the api below */
        if(!g_hash_table_contains(self->part_table, GINT_TO_POINTER(whole_disk))) {
            g_hash_table_insert(self->part_table,
                                GUINT_TO_POINTER(whole_disk),
                                rm_part_info_new(entry->dir, entry->fsname, whole_disk));
        }

        if(!g_hash_table_contains(self->disk_table, GINT_TO_POINTER(whole_disk))) {
            g_hash_table_insert(self->disk_table,
                                GINT_TO_POINTER(whole_disk),
                                rm_disk_info_new(diskname, is_rotational));
        }

        rm_log_debug_line(
            "%02u:%02u %50s -> %02u:%02u %-12s (underlying disk: %s; rotational: %3s)",
            major(stat_buf_folder.st_dev), minor(stat_buf_folder.st_dev), entry->dir,
            major(whole_disk), minor(whole_disk), entry->fsname, diskname,
            is_rotational ? "yes" : "no");
    }

    rm_mount_list_close(mnt_entries);
    return true;
}
Пример #16
0
static RmMountEntries *rm_mount_list_open(RmMountTable *table) {
    RmMountEntries *self = g_slice_new(RmMountEntries);

    self->mnt_entries = g_unix_mounts_get(NULL);
    self->entries = NULL;
    self->current = NULL;

    for(GList *iter = self->mnt_entries; iter; iter = iter->next) {
        RmMountEntry *wrap_entry = g_slice_new(RmMountEntry);
        GUnixMountEntry *entry = iter->data;

        wrap_entry->fsname = g_strdup(g_unix_mount_get_device_path(entry));
        wrap_entry->dir = g_strdup(g_unix_mount_get_mount_path(entry));
        wrap_entry->type = g_strdup(g_unix_mount_get_fs_type(entry));

        self->entries = g_list_prepend(self->entries, wrap_entry);
    }

    RmMountEntry *wrap_entry = NULL;
    while((wrap_entry = rm_mount_list_next(self))) {
        /* bindfs mounts mirror directory trees.
        * This cannot be detected properly by rmlint since
        * files in it have the same inode as their unmirrored file, but
        * a different dev_t.
        *
        * Also ignore kernel filesystems.
        *
        * So better go and ignore it.
        */
        static struct RmEvilFs {
            /* fsname as show by `mount` */
            const char *name;

            /* Wether to warn about the exclusion on this */
            bool unusual;
        } evilfs_types[] = {{"bindfs", 1},
                            {"nullfs", 1},
                            /* Ignore the usual linux file system spam */
                            {"proc", 0},
                            {"cgroup", 0},
                            {"configfs", 0},
                            {"sys", 0},
                            {"devtmpfs", 0},
                            {"debugfs", 0},
                            {NULL, 0}};

        /* btrfs and ocfs2 filesystems support reflinks for deduplication */
        static const char *reflinkfs_types[] = {"btrfs", "ocfs2", NULL};

        const struct RmEvilFs *evilfs_found = NULL;
        for(int i = 0; evilfs_types[i].name && !evilfs_found; ++i) {
            if(strcmp(evilfs_types[i].name, wrap_entry->type) == 0) {
                evilfs_found = &evilfs_types[i];
            }
        }

        const char *reflinkfs_found = NULL;
        for(int i = 0; reflinkfs_types[i] && !reflinkfs_found; ++i) {
            if(strcmp(reflinkfs_types[i], wrap_entry->type) == 0) {
                reflinkfs_found = reflinkfs_types[i];
                break;
            }
        }

        if(evilfs_found != NULL) {
            RmStat dir_stat;
            rm_sys_stat(wrap_entry->dir, &dir_stat);
            g_hash_table_insert(table->evilfs_table,
                                GUINT_TO_POINTER(dir_stat.st_dev),
                                GUINT_TO_POINTER(1));

            GLogLevelFlags log_level = G_LOG_LEVEL_DEBUG;

            if(evilfs_found->unusual) {
                log_level = G_LOG_LEVEL_WARNING;
                rm_log_warning_prefix();
            } else {
                rm_log_debug_prefix();
            }

            g_log("rmlint", log_level,
                  _("`%s` mount detected at %s (#%u); Ignoring all files in it.\n"),
                  evilfs_found->name, wrap_entry->dir, (unsigned)dir_stat.st_dev);
        }

        rm_log_debug_line("Filesystem %s: %s", wrap_entry->dir,
                          (reflinkfs_found) ? "reflink" : "normal");

        if(reflinkfs_found != NULL) {
            RmStat dir_stat;
            rm_sys_stat(wrap_entry->dir, &dir_stat);
            g_hash_table_insert(table->reflinkfs_table,
                                GUINT_TO_POINTER(dir_stat.st_dev),
                                (gpointer)reflinkfs_found);
        }
    }

    return self;
}