Learning to read(2)

Our previous assignment focused on file I/O, and this one will go deeper by combining what we know about C strings with file I/O.

Imagine you have a file that looks something like this (cat /proc/meminfo | head -n 5):

MemTotal:        1964472 kB
MemFree:          274432 kB
MemAvailable:    1578044 kB
Buffers:           60028 kB
Cached:          1288156 kB

…And you want to extract specific information from it. For example, maybe you want to read the MemAvailable field so you know how much memory is currently available on your machine.

To do this, you’d need to:

  1. Open the file,
  2. Read line by line,
  3. Look for the specific key we’re interested in, MemAvailable.
  4. When we find the key, extract its value, separated by delimiter(s). In this case, whitespace and : are the delimiters.
  5. Copy the value back to a user-supplied buffer (now filled with 1578044 kB) and return a value to indicate success.
  6. Rewind the file so that the next call to our function will consider all possible keys again.

That’s actually exactly what you’ll do in this lab. But the situation gets more interesting, because you have a few restrictions to deal with:

  1. Instead of using the f- functions from the previous lab (such as fgets), you’ll use the lower-level open(2) and read(2) system calls. Check out the man pages for a refresher on how they work.
  2. You’ll implement your own version of fgets using read(2)
  3. If you research how to handle the delimiters, you may find some options in the C standard library. However, you’re not allowed to use them in this assignment, so you’ll need to implement delimiter handling yourself.

Finally, to tie this all together, you need a way to test your new functions. To do this, create a command line utility called lookup that will search for a key in a given file and report its value(s). For example:

$ ./lookup MemAvailable ': ' /proc/meminfo
1578044 kB
$ ./lookup Buffers ': ' /proc/meminfo
60028 kB
$ ./lookup buffers ': ' /proc/meminfo
$ # (nothing prints, since the search is case-sensitive)

We’re passing in the key, delimiters (in quotes so spaces are included), and a list of file(s) to search.

Implementation Details

You can name your function however you’d like and the implementation details are up to you. Here’s one example:

int lookup_key(int fd, char *key, char* buf, size_t buf_sz);

Where the ‘buf’ contains the value that was found (if any), and the return value is used to report errors. Be sure to fully document your function so it’s easy to understand during code review.

For your implementation of fgets, you have the same level of freedom, but since we are working with lower-level functionality you’ll need to accept a file descriptor (fd) as one of the arguments.

Hint 1: at a very basic level, fgets is reading character by character until it finds \n or fills its buffer. Make sure the buffer is always null-terminated! This means that if a buffer has a 100-character capacity, only 99 characters maximum can be used for storing the line.

Hint 2: your lookup_key implementation should be able to handle situations where the buffer fills up before a newline is found. You can test this by inspecting the second-to-last character in the buffer; if it’s not a newline, then you haven’t reached the end of the line yet. If that is the case, read until you reach a newline character so that the rest of the line isn’t considered a key during the next call to lookup_key, and ignore any excess characters beyond the buffer limit.

Testing

Craft at least 3 test cases to evaluate your implementation of the lookup command line utility. Store them in a shell script called test.sh. Here’s an example:

#!/usr/bin/env bash

./lookup badkey ': ' /proc/meminfo                # No lines match
./lookup MemAvailable 'baddelim' /proc/meminfo    # Won't remove ':     ' before key
./lookup MemAvailable ': ' /proc/meminfo          # Should work and find the value for MemAvailable

Add your 3 test cases to the script and provide a file called expected-output.txt to show what the output should look like.

Grading and Submission

Check your code into lab2 within your lab repo. Provide a Makefile that builds the program (please use the -Wall option), and also include clean and install recipes (install to /usr/local/bin). Additionally, you must have a new memcheck recipe in your Makefile that uses valgrind to find memory and file descriptor errors. Here’s an example:

memcheck: lookup
    valgrind \
        --track-fds=yes \
        --track-origins=yes \
        --leak-check=full \
        --trace-children=yes ./test.sh

To receive 70% credit:

To receive 90% credit:

To receive 100% credit: