This lab will have you make a non-interactive command-line utility. This kind of program is an extremely common kind of thing to write in C. Many of the command-line utilities you’ve used so far (e.g. ls, cd, mv) are like this.

This program will give you practice with using the C file API (fopen, fread, fwrite, fseek, ftell, and fclose) and help you learn about what is in binary files.


1. Getting started

  1. Right click and copy this link.
  2. ssh into thoth, cd to your labs directory, and use wget to download this file to thoth like you did on lab 1.
  3. Run unzip lab3.zip on thoth, and it will unzip all the files for this lab.
  4. Rename abc123_lab3.c to your username with mv: e.g. mv abc123_lab3.c jfb42_lab3.c
  5. Connect to thoth in your SFTP client, navigate to your labs directory, and open your _lab3.c file for editing locally.

Now you’re ready!

Compiling and running

I’ve given you some code to get started. On thoth:

gcc -Wall -Werror --std=c99 -o lab3 abc123_lab3.c

If you run ./lab3 now, it will print out directions on how to use it. This is called usage info and most command-line programs will print something like this. If you look at the source code, main is at the end of the file, and you can see that it prints this info out if the command-line arguments aren’t of the correct form.

What is this program?

This is a very simplistic “food database” program that can create, display, and add new food items to “food database” files. Something like a simplistic version of what a grocery store would use for stock keeping and pricing purposes.

A food database file contains a list of food items, each of which has a name, a department, a price, and a stock. This is a binary file, not a text file, so you cannot read/edit it in a text editor. That’s why this program exists!

Binary files

All the .db files given to you are binary files. They are a custom format created for this assignment. The format is:

char magic[4] = "FOOD"; // notice no 0 terminator, just 4 characters
int endianness;         // 1 for little, 0 for big

// then 0 or more food items, each of which is 32 bytes and consists of:
  char name[16]; // includes 0 terminator, so never more than 15 characters
  int dept;      // one of the non-Invalid values of the Department enum
  double price;  // >= 0.01
  unsigned int stock;

Other examples of magic numbers are .gif files which start with GIF and .png files which start with the byte 0x89, followed by PNG. Many, many more binary file formats are documented on this site.

The char magic[4] is the magic number that identifies this as a food database file. Many binary file formats start with some kind of unique sequence of bytes that indicates to programs that yes, this really is a file of the right type. Because remember, the file extension doesn’t mean anything!

Run hexdump -C good.db (that’s capital -C, not lowercase -c. case matters!). You’ll see some output that starts with:

00000000  46 4f 4f 44 01 00 00 00  42 72 65 61 64 00 00 00  |FOOD....Bread...|
00000010  00 00 00 00 00 00 00 00  01 00 00 00 d7 a3 70 3d  |..............p=|
00000020  0a d7 ff 3f 32 00 00 00  44 6f 75 67 68 6e 75 74  |...?2...Doughnut|
00000030  00 00 00 00 00 00 00 00  01 00 00 00 ae 47 e1 7a  |.............G.z|

The leftmost column is the file offset, shown in hexadecimal. Each row displays 16 bytes, so that’s why the offset is increasing by 16 (0x10) on each row.

The middle columns are the actual bytes of the file, with each byte displayed as a 2-digit hex number.

The right column shows those same bytes interpreted as printable ASCII if possible, or as . if not. For example, you can see the bytes 46 4f 4f 44 are interpreted as FOOD. But right after that are the bytes 01 00 00 00, which aren’t printable ASCII characters, so they’re shown as .....

There are bits of text mixed in here and there, but most of the file is non-text data that is unreadable to us. You can kind of puzzle out what the data is knowing the file format from above, but it’s not easy.

Your goal

Your program will let you create new empty .db files, view the contents of .db files in a couple ways, and add new food items to the ends of them.

Although simple, it will show you the basics of using binary files, parsing and validating command-line arguments, and giving good error messages.

I have already given you some code to check which command-line arguments were passed, and you have to write all the actual database manipulation.

Your program will be able to:

  1. Create new database files, by using ./lab3 new filename.
  2. Print all the foods in a database file, by using ./lab3 print filename.
  3. Print the foods in one department, by using ./lab3 printdept filename deptname.
  4. Add a new food, by using ./lab3 add filename deptname name price stock.
  5. Give the user good errors when they give invalid inputs:

     $ ./lab3 print ahsifahsoihagh
     could not open file.
     $ ./lab3 print bad_magic.db
     file doesn't appear to be a food database (bad magic).
     $ ./lab3 print bad_endian.db
     file doesn't appear to be a food database (bad endianness).
     $ ./lab3 print bad_length.db
     file doesn't appear to be a food database (bad length).
     $ ./lab3 printdept good.db nfasfpoansi
     invalid department name!
     $
    

The fatal() function

There is a function given to you called fatal(). It’s very simple: it takes a string, prints that string to stderr (which by default looks the same as printing it to stdout), then exits the program with an exit code of 1.

This is a quick-and-dirty way of “falling over and dying” which many Java programs implement by throwing an exception that escapes main. It’s not the most graceful way of handling errors, but that’s the nice thing about writing command-line tools - no one expects your program to keep running if something goes wrong! So just exit! It’s fine!


A note on debugging when you have command-line arguments

If your program crashes, you may try running it in gdb like this, and get a confusing error about “no such file or directory:”

$ gdb ./lab3 print bad_length.db
...
/afs/pitt.edu/home/a/b/abc123/private/cs0449/labs/print: No such file or directory.
(gdb)

That’s weird. And when you try to use the run command, it just prints out the usage.

You have to run gdb a different way to pass command-line arguments to your program:

$ gdb --args ./lab3 print bad_length.db

Now when you run, it will pass those arguments to your program.


Your job

You will implement the code in lab3.c. These are the functions you will be filling in:

Do not try to do everything first, and then try to compile and run. Follow the instructions below to see how to test your work at each step.


2. Implementing ./lab3 new filename

  1. Implement new_database by following the comments inside it.
  2. Compile it like before: gcc -Wall -Werror --std=c99 -o lab3 abc123_lab3.c
  3. Run it like ./lab3 new mine.db. It should not appear to do anything, but…
  4. ls -l should show that mine.db exists and has a size of 8 like:

     -rw-r--r-- 1 abc123 UNKNOWN1     8 Sep 14 15:40 mine.db
    
    • (That 8 before Sep is the file size in bytes.)
  5. hexdump -C mine.db should show:

     00000000  46 4f 4f 44 01 00 00 00                           |FOOD....|
     00000008
    
  6. If you do ./lab3 new mine.db again, it should say “could not create database file.”.
    • this prevents you from overwriting existing databases.

If the output isn’t exactly what is shown above, you need to fix something! It should be exactly 8 bytes long, and they should have the values shown above.


3. Implementing ./lab3 print filename

  1. implement open_database.
    • This is a long one! But it’s important that we do a lot of checking so that we are fairly confident that we really are looking at a food database file.
  2. implement read_food.
    • This is much shorter.
  3. implement print_all_foods.
    • This is really short!

When you run ./lab3 print good.db it should output:

$ ./lab3 print good.db
[Bakery ] Bread ($1.99 each, 50 in stock)
[Bakery ] Doughnut ($0.99 each, 100 in stock)
[Bakery ] Bagel ($1.99 each, 47 in stock)
[Canned ] Soup ($2.99 each, 24 in stock)
[Canned ] Cat Food ($1.19 each, 80 in stock)
[Canned ] Dog Food ($2.29 each, 54 in stock)
[Frozen ] Ice Cream ($5.99 each, 108 in stock)
[Frozen ] Blueberries ($7.49 each, 19 in stock)
[Frozen ] Pizza Rolls ($4.99 each, 14 in stock)
[Produce] Apple ($0.99 each, 15985 in stock)
[Produce] Banana ($0.39 each, 7777 in stock)
[Produce] Lettuce ($1.49 each, 68 in stock)
$

with no segmentation faults at the end or anything.

If you try to run it on the other database files (or on a file that doesn’t exist), they should all fail with specific error messages:

$ ./lab3 print aojsfoaisfioahs
could not open file.
$ ./lab3 print bad_magic.db
file doesn't appear to be a food database (bad magic).
$ ./lab3 print bad_endian.db
file doesn't appear to be a food database (bad endianness).
$ ./lab3 print big_endian.db
big-endian databases are unsupported.
$ ./lab3 print bad_length.db
file doesn't appear to be a food database (bad length).
$

4. Implementing ./lab3 printdept filename deptname

Just need to implement print_foods_in_department. It’s very similar to print_all_foods.

Test it with the four departments (bakery, canned, frozen, produce) and some invalid ones too:

$ ./lab3 printdept good.db bakery
[Bakery ] Bread ($1.99 each, 50 in stock)
[Bakery ] Doughnut ($0.99 each, 100 in stock)
[Bakery ] Bagel ($1.99 each, 47 in stock)
$ ./lab3 printdept good.db CANNED
[Canned ] Soup ($2.99 each, 24 in stock)
[Canned ] Cat Food ($1.19 each, 80 in stock)
[Canned ] Dog Food ($2.29 each, 54 in stock)
$ ./lab3 printdept good.db cats
invalid department name!
$

5. Implementing ./lab3 add filename deptname name price stock

add_food has a lot of correctness checks you need to do. Notes:

Now you should be able to add foods to any database and see them when you use ./lab3 print on it. Importantly, adding a food should not corrupt the database:

$ ./lab3 print mine.db
$ ./lab3 add mine.db bakery "Apple Pie" 4.99 12
$ ./lab3 print mine.db
[Bakery ] Apple Pie ($4.99 each, 12 in stock)
$ ./lab3 add mine.db frozen Peas 1.99 40
$ ./lab3 print mine.db
[Bakery ] Apple Pie ($4.99 each, 12 in stock)
[Frozen ] Peas ($1.99 each, 40 in stock)
$ ./lab3 add mine.db produce Kiwi 0.99 500
$ ./lab3 print mine.db
[Bakery ] Apple Pie ($4.99 each, 12 in stock)
[Frozen ] Peas ($1.99 each, 40 in stock)
[Produce] Kiwi ($0.99 each, 500 in stock)
$

(yes, you can use “double quoted strings” in bash too)

Be sure to test all the correctness checks, too.

$ ./lab3 add mine.db asjfojas fojas foja sofj
invalid department name!
$ ./lab3 add mine.db bakery fojas foja sofj
price is not formatted correctly!
$ ./lab3 add mine.db bakery fojas 0 sofj
invalid price!
$ ./lab3 add mine.db bakery fojas 10 sofj
stock is not formatted correctly!
$ ./lab3 add mine.db bakery fojas 10 -5
invalid stock!
$ ./lab3 add mine.db bakery 1234567890123456 10 100
item name too long!
$ ./lab3 add mine.db bakery "" 10 100
item name too short!
$

Submission

We will be compiling your program with these compiler options, so make sure your program compiles with them:

gcc -Wall -Werror --std=c99 -o lab3 abc123_lab3.c

Just like before:

  1. Download your _lab3.c file to your computer with your SFTP client.
  2. Go to the canvas for this course.
  3. Click Gradescope on the left.
  4. Click on Lab 3.
  5. Drag your _lab3.c file into the Gradescope submission thingy.
  6. Wait for the autograder to complete!

If the autograder says something is wrong with your submission, fix it on thoth, re-download it to your computer , and re-submit it as many times as needed to fix the problem.

Note that future assignments may have a limited number (or rate) of resubmissions, so do not rely on the autograder to be your only method of finding and fixing problems!