| /******************************************************************************* | |
| Copyright (c) 2011, 2012 Dmitry Matveev <me@dmitrymatveev.co.uk> | |
| Permission is hereby granted, free of charge, to any person obtaining a copy | |
| of this software and associated documentation files (the "Software"), to deal | |
| in the Software without restriction, including without limitation the rights | |
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| copies of the Software, and to permit persons to whom the Software is | |
| furnished to do so, subject to the following conditions: | |
| The above copyright notice and this permission notice shall be included in | |
| all copies or substantial portions of the Software. | |
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
| THE SOFTWARE. | |
| *******************************************************************************/ | |
| #include <glib.h> | |
| #include <stdlib.h> /* calloc */ | |
| #include <stdio.h> /* printf */ | |
| #include <dirent.h> /* opendir, readdir, closedir */ | |
| #include <string.h> /* strcmp */ | |
| #include <assert.h> | |
| #include "dep-list.h" | |
| static gboolean kdl_debug_enabled = FALSE; | |
| #define perror_msg if (kdl_debug_enabled) g_warning | |
| /** | |
| * Print a list to stdout. | |
| * | |
| * @param[in] dl A pointer to a list. | |
| **/ | |
| void | |
| dl_print (const dep_list *dl) | |
| { | |
| while (dl != NULL) { | |
| printf ("%lld:%s ", (long long int) dl->inode, dl->path); | |
| dl = dl->next; | |
| } | |
| printf ("\n"); | |
| } | |
| /** | |
| * Create a new list item. | |
| * | |
| * Create a new list item and initialize its fields. | |
| * | |
| * @param[in] path A name of a file (the string is not copied!). | |
| * @param[in] inode A file's inode number. | |
| * @return A pointer to a new item or NULL in the case of error. | |
| **/ | |
| dep_list* dl_create (char *path, ino_t inode) | |
| { | |
| dep_list *dl = calloc (1, sizeof (dep_list)); | |
| if (dl == NULL) { | |
| perror_msg ("Failed to create a new dep-list item"); | |
| return NULL; | |
| } | |
| dl->path = path; | |
| dl->inode = inode; | |
| return dl; | |
| } | |
| /** | |
| * Create a shallow copy of a list. | |
| * | |
| * A shallow copy is a copy of a structure, but not the copy of the | |
| * contents. All data pointers ('path' in our case) of a list and its | |
| * shallow copy will point to the same memory. | |
| * | |
| * @param[in] dl A pointer to list to make a copy. May be NULL. | |
| * @return A shallow copy of the list. | |
| **/ | |
| dep_list* | |
| dl_shallow_copy (const dep_list *dl) | |
| { | |
| dep_list *head; | |
| dep_list *cp; | |
| const dep_list *it; | |
| if (dl == NULL) { | |
| return NULL; | |
| } | |
| head = calloc (1, sizeof (dep_list)); | |
| if (head == NULL) { | |
| perror_msg ("Failed to allocate head during shallow copy"); | |
| return NULL; | |
| } | |
| cp = head; | |
| it = dl; | |
| while (it != NULL) { | |
| cp->path = it->path; | |
| cp->inode = it->inode; | |
| if (it->next) { | |
| cp->next = calloc (1, sizeof (dep_list)); | |
| if (cp->next == NULL) { | |
| perror_msg ("Failed to allocate a new element during shallow copy"); | |
| dl_shallow_free (head); | |
| return NULL; | |
| } | |
| cp = cp->next; | |
| } | |
| it = it->next; | |
| } | |
| return head; | |
| } | |
| /** | |
| * Free the memory allocated for shallow copy. | |
| * | |
| * This function will free the memory used by a list structure, but | |
| * the list data will remain in the heap. | |
| * | |
| * @param[in] dl A pointer to a list. May be NULL. | |
| **/ | |
| void | |
| dl_shallow_free (dep_list *dl) | |
| { | |
| while (dl != NULL) { | |
| dep_list *ptr = dl; | |
| dl = dl->next; | |
| free (ptr); | |
| } | |
| } | |
| /** | |
| * Free the memory allocated for a list. | |
| * | |
| * This function will free all the memory used by a list: both | |
| * list structure and the list data. | |
| * | |
| * @param[in] dl A pointer to a list. May be NULL. | |
| **/ | |
| void | |
| dl_free (dep_list *dl) | |
| { | |
| while (dl != NULL) { | |
| dep_list *ptr = dl; | |
| dl = dl->next; | |
| free (ptr->path); | |
| free (ptr); | |
| } | |
| } | |
| /** | |
| * Create a directory listing and return it as a list. | |
| * | |
| * @param[in] path A path to a directory. | |
| * @return A pointer to a list. May return NULL, check errno in this case. | |
| **/ | |
| dep_list* | |
| dl_listing (const char *path) | |
| { | |
| dep_list *head = NULL; | |
| dep_list *prev = NULL; | |
| DIR *dir; | |
| assert (path != NULL); | |
| dir = opendir (path); | |
| if (dir != NULL) { | |
| struct dirent *ent; | |
| while ((ent = readdir (dir)) != NULL) { | |
| dep_list *iter; | |
| if (!strcmp (ent->d_name, ".") || !strcmp (ent->d_name, "..")) { | |
| continue; | |
| } | |
| if (head == NULL) { | |
| head = calloc (1, sizeof (dep_list)); | |
| if (head == NULL) { | |
| perror_msg ("Failed to allocate head during listing"); | |
| goto error; | |
| } | |
| } | |
| iter = (prev == NULL) ? head : calloc (1, sizeof (dep_list)); | |
| if (iter == NULL) { | |
| perror_msg ("Failed to allocate a new element during listing"); | |
| goto error; | |
| } | |
| iter->path = strdup (ent->d_name); | |
| if (iter->path == NULL) { | |
| perror_msg ("Failed to copy a string during listing"); | |
| goto error; | |
| } | |
| iter->inode = ent->d_ino; | |
| iter->next = NULL; | |
| if (prev) { | |
| prev->next = iter; | |
| } | |
| prev = iter; | |
| } | |
| closedir (dir); | |
| } | |
| return head; | |
| error: | |
| if (dir != NULL) { | |
| closedir (dir); | |
| } | |
| dl_free (head); | |
| return NULL; | |
| } | |
| /** | |
| * Perform a diff on lists. | |
| * | |
| * This function performs something like a set intersection. The same items | |
| * will be removed from the both lists. Items are comapred by a filename. | |
| * | |
| * @param[in,out] before A pointer to a pointer to a list. Will contain items | |
| * which were not found in the 'after' list. | |
| * @param[in,out] after A pointer to a pointer to a list. Will contain items | |
| * which were not found in the 'before' list. | |
| **/ | |
| void | |
| dl_diff (dep_list **before, dep_list **after) | |
| { | |
| dep_list *before_iter; | |
| dep_list *before_prev; | |
| assert (before != NULL); | |
| assert (after != NULL); | |
| if (*before == NULL || *after == NULL) { | |
| return; | |
| } | |
| before_iter = *before; | |
| before_prev = NULL; | |
| while (before_iter != NULL) { | |
| dep_list *after_iter = *after; | |
| dep_list *after_prev = NULL; | |
| dep_list *oldptr; | |
| int matched = 0; | |
| while (after_iter != NULL) { | |
| if (strcmp (before_iter->path, after_iter->path) == 0) { | |
| matched = 1; | |
| /* removing the entry from the both lists */ | |
| if (before_prev) { | |
| before_prev->next = before_iter->next; | |
| } else { | |
| *before = before_iter->next; | |
| } | |
| if (after_prev) { | |
| after_prev->next = after_iter->next; | |
| } else { | |
| *after = after_iter->next; | |
| } | |
| free (after_iter); | |
| break; | |
| } | |
| after_prev = after_iter; | |
| after_iter = after_iter->next; | |
| } | |
| oldptr = before_iter; | |
| before_iter = before_iter->next; | |
| if (matched == 0) { | |
| before_prev = oldptr; | |
| } else { | |
| free (oldptr); | |
| } | |
| } | |
| } | |
| /** | |
| * Traverses two lists. Compares items with a supplied expression | |
| * and performs the passed code on a match. Removes the matched entries | |
| * from the both lists. | |
| **/ | |
| #define EXCLUDE_SIMILAR(removed_list, added_list, match_expr, matched_code) \ | |
| G_STMT_START { \ | |
| dep_list *removed_list##_iter; \ | |
| dep_list *removed_list##_prev; \ | |
| int productive = 0; \ | |
| \ | |
| assert (removed_list != NULL); \ | |
| assert (added_list != NULL); \ | |
| \ | |
| removed_list##_iter = *removed_list; \ | |
| removed_list##_prev = NULL; \ | |
| \ | |
| while (removed_list##_iter != NULL) { \ | |
| dep_list *added_list##_iter = *added_list; \ | |
| dep_list *added_list##_prev = NULL; \ | |
| dep_list *oldptr; \ | |
| \ | |
| int matched = 0; \ | |
| while (added_list##_iter != NULL) { \ | |
| if (match_expr) { \ | |
| matched = 1; \ | |
| ++productive; \ | |
| matched_code; \ | |
| \ | |
| if (removed_list##_prev) { \ | |
| removed_list##_prev->next = removed_list##_iter->next; \ | |
| } else { \ | |
| *removed_list = removed_list##_iter->next; \ | |
| } \ | |
| if (added_list##_prev) { \ | |
| added_list##_prev->next = added_list##_iter->next; \ | |
| } else { \ | |
| *added_list = added_list##_iter->next; \ | |
| } \ | |
| free (added_list##_iter); \ | |
| break; \ | |
| } \ | |
| added_list##_iter = added_list##_iter->next; \ | |
| } \ | |
| oldptr = removed_list##_iter; \ | |
| removed_list##_iter = removed_list##_iter->next; \ | |
| if (matched == 0) { \ | |
| removed_list##_prev = oldptr; \ | |
| } else { \ | |
| free (oldptr); \ | |
| } \ | |
| } \ | |
| return (productive > 0); \ | |
| } G_STMT_END | |
| #define cb_invoke(cbs, name, udata, ...) \ | |
| do { \ | |
| if (cbs->name) { \ | |
| (cbs->name) (udata, ## __VA_ARGS__); \ | |
| } \ | |
| } while (0) | |
| /** | |
| * Detect and notify about moves in the watched directory. | |
| * | |
| * A move is what happens when you rename a file in a directory, and | |
| * a new name is unique, i.e. you didn't overwrite any existing files | |
| * with this one. | |
| * | |
| * @param[in,out] removed A list of the removed files in the directory. | |
| * @param[in,out] added A list of the added files of the directory. | |
| * @param[in] cbs A pointer to #traverse_cbs, a user-defined set of | |
| * traverse callbacks. | |
| * @param[in] udata A pointer to the user-defined data. | |
| * @return 0 if no files were renamed, >0 otherwise. | |
| **/ | |
| static int | |
| dl_detect_moves (dep_list **removed, | |
| dep_list **added, | |
| const traverse_cbs *cbs, | |
| void *udata) | |
| { | |
| assert (cbs != NULL); | |
| EXCLUDE_SIMILAR | |
| (removed, added, | |
| (removed_iter->inode == added_iter->inode), | |
| { | |
| cb_invoke (cbs, moved, udata, | |
| removed_iter->path, removed_iter->inode, | |
| added_iter->path, added_iter->inode); | |
| }); | |
| } | |
| /** | |
| * Detect and notify about replacements in the watched directory. | |
| * | |
| * Consider you are watching a directory foo with the following files | |
| * insinde: | |
| * | |
| * foo/bar | |
| * foo/baz | |
| * | |
| * A replacement in a watched directory is what happens when you invoke | |
| * | |
| * mv /foo/bar /foo/bar | |
| * | |
| * i.e. when you replace a file in a watched directory with another file | |
| * from the same directory. | |
| * | |
| * @param[in,out] removed A list of the removed files in the directory. | |
| * @param[in,out] current A list with the current contents of the directory. | |
| * @param[in] cbs A pointer to #traverse_cbs, a user-defined set of | |
| * traverse callbacks. | |
| * @param[in] udata A pointer to the user-defined data. | |
| * @return 0 if no files were renamed, >0 otherwise. | |
| **/ | |
| static int | |
| dl_detect_replacements (dep_list **removed, | |
| dep_list **current, | |
| const traverse_cbs *cbs, | |
| void *udata) | |
| { | |
| assert (cbs != NULL); | |
| EXCLUDE_SIMILAR | |
| (removed, current, | |
| (removed_iter->inode == current_iter->inode), | |
| { | |
| cb_invoke (cbs, replaced, udata, | |
| removed_iter->path, removed_iter->inode, | |
| current_iter->path, current_iter->inode); | |
| }); | |
| } | |
| /** | |
| * Detect and notify about overwrites in the watched directory. | |
| * | |
| * Consider you are watching a directory foo with a file inside: | |
| * | |
| * foo/bar | |
| * | |
| * And you also have a directory tmp with a file 1: | |
| * | |
| * tmp/1 | |
| * | |
| * You do not watching directory tmp. | |
| * | |
| * An overwrite in a watched directory is what happens when you invoke | |
| * | |
| * mv /tmp/1 /foo/bar | |
| * | |
| * i.e. when you overwrite a file in a watched directory with another file | |
| * from the another directory. | |
| * | |
| * @param[in,out] previous A list with the previous contents of the directory. | |
| * @param[in,out] current A list with the current contents of the directory. | |
| * @param[in] cbs A pointer to #traverse_cbs, a user-defined set of | |
| * traverse callbacks. | |
| * @param[in] udata A pointer to the user-defined data. | |
| * @return 0 if no files were renamed, >0 otherwise. | |
| **/ | |
| static int | |
| dl_detect_overwrites (dep_list **previous, | |
| dep_list **current, | |
| const traverse_cbs *cbs, | |
| void *udata) | |
| { | |
| assert (cbs != NULL); | |
| EXCLUDE_SIMILAR | |
| (previous, current, | |
| (strcmp (previous_iter->path, current_iter->path) == 0 | |
| && previous_iter->inode != current_iter->inode), | |
| { | |
| cb_invoke (cbs, overwritten, udata, current_iter->path, current_iter->inode); | |
| }); | |
| } | |
| /** | |
| * Traverse a list and invoke a callback for each item. | |
| * | |
| * @param[in] list A #dep_list. | |
| * @param[in] cb A #single_entry_cb callback function. | |
| * @param[in] udata A pointer to the user-defined data. | |
| **/ | |
| static void | |
| dl_emit_single_cb_on (dep_list *list, | |
| single_entry_cb cb, | |
| void *udata) | |
| { | |
| while (cb && list != NULL) { | |
| (cb) (udata, list->path, list->inode); | |
| list = list->next; | |
| } | |
| } | |
| /** | |
| * Recognize all the changes in the directory, invoke the appropriate callbacks. | |
| * | |
| * This is the core function of directory diffing submodule. | |
| * | |
| * @param[in] before The previous contents of the directory. | |
| * @param[in] after The current contents of the directory. | |
| * @param[in] cbs A pointer to user callbacks (#traverse_callbacks). | |
| * @param[in] udata A pointer to user data. | |
| **/ | |
| void | |
| dl_calculate (dep_list *before, | |
| dep_list *after, | |
| const traverse_cbs *cbs, | |
| void *udata) | |
| { | |
| int need_update = 0; | |
| dep_list *was = dl_shallow_copy (before); | |
| dep_list *pre = dl_shallow_copy (before); | |
| dep_list *now = dl_shallow_copy (after); | |
| dep_list *lst = dl_shallow_copy (after); | |
| assert (cbs != NULL); | |
| dl_diff (&was, &now); | |
| need_update += dl_detect_moves (&was, &now, cbs, udata); | |
| need_update += dl_detect_replacements (&was, &lst, cbs, udata); | |
| dl_detect_overwrites (&pre, &lst, cbs, udata); | |
| if (need_update) { | |
| cb_invoke (cbs, names_updated, udata); | |
| } | |
| dl_emit_single_cb_on (was, cbs->removed, udata); | |
| dl_emit_single_cb_on (now, cbs->added, udata); | |
| cb_invoke (cbs, many_added, udata, now); | |
| cb_invoke (cbs, many_removed, udata, was); | |
| dl_shallow_free (lst); | |
| dl_shallow_free (now); | |
| dl_shallow_free (pre); | |
| dl_shallow_free (was); | |
| } |