diff --git a/14-getdents/letters.c b/14-getdents/letters.c
index 0ac1e3c..361c35c 100644
--- a/14-getdents/letters.c
+++ b/14-getdents/letters.c
@@ -16,10 +16,34 @@
// Christmas, we have to know the timestamp of Christmas. We derive
// that timestamp from the current time.
time_t christmas_day(int delta_year) {
- // FIXME: calculate timestamp for Christmas.
- // delta_years = 0 => this Christmas
- // delta_years = -1 => last Christmas
- return time(NULL);
+ // Get the current UNIX time
+ time_t now = time(NULL);
+
+ // Break down the timestamp (according to localtime) into days,
+ // months, years with localtime_r. We use localtime_r, because
+ // nobody should use glibc functions that have an internal state
+ // and return us a pointer to it!
+ struct tm tm;
+ if (localtime_r(&now, &tm) == NULL)
+ die("localtime_r");
+
+ // Day, month, year
+ if (tm.tm_mday > 24 && tm.tm_mon == 11)
+ delta_year += 1; // The days after christmas
+ tm.tm_mday = 24;
+ tm.tm_mon = 11;
+ tm.tm_year += delta_year;
+
+ // Time: Mid of day
+ tm.tm_hour = 12;
+ tm.tm_min = 0;
+ tm.tm_sec = 0;
+
+ // Derive DST information from local timezone
+ tm.tm_isdst = -1;
+
+ // Create an UNIX timestamp
+ return mktime(&tm);
}
// For us, a letter consists of a timestamp and a pointer to a
@@ -30,18 +54,127 @@ struct letter {
};
+// For qsort(3), we require a compare function that compares two
+// letters. We compare those letters according to their timestamp.
+// Oldest first.
+int letter_compare(const void* _A, const void *_B) {
+ const struct letter *A = _A;
+ const struct letter *B = _B;
+
+ if (A->timestamp < B->timestamp)
+ return -1;
+ if (A->timestamp > B->timestamp)
+ return 1;
+ return 0;
+}
int main(int argc, char *argv[]) {
- // FIXME: Open the directory with open(2) and O_DIRECTORY
- // FIXME: Read directory entries with getdents64(2)
- // FIXME: For each entry, use statx(2) to extract the birthtime
- // FIXME: Sort by birthtime with qsort(3)
- // FIXME: Output letters that were created after the last christmas
- // and before the next christmas in the following format
- // (151 until the next Christmas).
-
- // $ ./letters
- // 151 days: Makefile
- // 151 days: letters
+ // If no argument was given, we crawl the current directory for
+ // letters. Please note that it is _NOT_ necessary to recursively
+ // crawl the directory.
+ char *dirname = argc > 1 ? argv[1] : ".";
+
+ // We open the directory with open. As it is a directory, we have
+ // to specify O_DIRECTORY to let the open succeed. Thereby, Linux
+ // ensures that you not accidentally open a directory as a
+ // regular file. This is a difference to BSD systems, there
+ // opening (or even using cat) on a directory just works.
+ int dirfd = open(dirname, O_RDONLY | O_DIRECTORY);
+ if (dirfd == -1)
+ die("open");
+
+ // We use growable array, by an element counter, an capacity
+ // counter, and an pointer to an heap allocated piece of memory.
+ struct letter *letters = NULL;
+ int nletters = 0;
+ int capacity = 0;
+
+
+ // We calculate UNIX timestamps for the next and the previous
+ // Christmas.
+ time_t last_christmas = christmas_day(-1);
+ time_t this_christmas = christmas_day(0);
+
+ for (;;) {
+ // We use getdents64(2) to read directory entries from the
+ // directory file descriptor. The structure is explained in
+ // getdents64(2).
+ char buf[4096];
+ ssize_t len = getdents64(dirfd, buf, sizeof(buf));
+ if (len == -1) die("getdents");
+ if (len == 0) // EOF
+ break;
+
+ // We iterate over all directory entries. Please note that
+ // those entries are not of fixed size, but each entry is
+ // d->d_reclen bytes long. Therefore, we require a special
+ // iteration technique.
+ struct dirent64 *d;
+ for (long bpos = 0; bpos < len; bpos += d->d_reclen) {
+ d = (struct dirent64 *) (buf + bpos);
+
+ // We skip everything that is not an regular file.
+ if (d->d_type != DT_REG)
+ continue;
+
+ // We use statx(2) to get the birth time of the file.
+ // Here are two things to notice:
+ //
+ // 1. We use the directory file descriptor instead of
+ // giving a regular path. This is great as it avoids
+ // the time-of-check-time-of-use (TOCTOU) problem that
+ // would arise otherwise.
+ // 2. We specifically request the BTIME from the kernel.
+ struct statx s;
+ int rc = statx(dirfd, d->d_name, 0, STATX_BTIME, &s);
+ if (rc < 0)
+ die("statx");
+
+ // The kernel also gives us the information whether the
+ // queries stat entry contains a btime or not. Because not
+ // all file systems support recording an birth time. For
+ // example, the /proc file system does not return birth
+ // times here (try it!)
+ if (!(s.stx_mask & STATX_BTIME))
+ die("statx: file system does not support btime");
+
+ // We skip all files that were born before the last or
+ // after the next Christmas
+ if (!(last_christmas < s.stx_btime.tv_sec
+ && s.stx_btime.tv_sec < this_christmas))
+ continue;
+
+ // Grow the dynamic array if necessary.
+ if (capacity < (nletters + 1)) {
+ letters = reallocarray(letters,
+ nletters + capacity + 1,
+ sizeof(struct letter));
+ if (!letters) die("realloc");
+ capacity += nletters + 1;
+ }
+
+ // Insert the letter into our letter array. For this, we
+ // _have_ to use strdup for the filename as the filename
+ // is _within_ our own `buf', which we will overwrite
+ // with the next getdents64(2) call.
+ letters[nletters].timestamp = s.stx_btime.tv_sec;
+ letters[nletters].filename = strdup(d->d_name);
+ if (!letters[nletters].filename)
+ die("strdup");
+ nletters ++;
+ }
+ }
+
+ // Sort the letters. This is easy. qsort(3) will sort our letters in-place.
+ qsort(letters, nletters, sizeof(struct letter),
+ letter_compare);
+
+
+ // Print out the letters according to the desired format.
+ for (unsigned int i = 0; i < nletters; i++) {
+ printf("%ld days: %s\n",
+ (this_christmas - letters[i].timestamp) / (24 * 3600),
+ letters[i].filename);
+ }
}