CS210 · Block 5 · Lesson 37

Defensive Programming and Reading Existing C Code

Assertions · Error codes & errno · Input validation · Reading code by other authors
At a glance: You have been writing defensive code in pieces since Lesson 16. Today it comes together into one habit, and you add the skill that defines real engineering work: reading and hardening code you did not write. You practice through two widgets in Section 5, then harden a marketplace inventory loader a teammate left you. The organizing rule for the whole lesson: assertions catch your bugs; validation handles bad input.
Contents
  1. Lesson Objectives
  2. Assigned Readings
  3. Pre-Class Work
  4. Lesson Materials and Overview
  5. In-Class Work
  6. After Class
1. Lesson Objectives

By the end of this lesson, cadets will be able to:

  1. Apply assertions to validate program assumptions during development. (Outcome 1)
  2. Implement error handling using error codes and errno. (Outcomes 1, 3)
  3. Validate input against expected ranges and formats. (Outcomes 1, 3)
  4. Read and interpret a non-trivial C codebase written by other authors. (Outcome 2)
2. Assigned Readings

There is no new Beej chapter for this lesson. It consolidates defensive habits you have practiced since Lesson 16, so the reading is focused and short:

  • The Lesson 37 supplementary notes (linked in the course materials): a self-contained walk through reading existing code, assertions, error codes and errno, and input validation.
  • The provided loader source you will harden in the lab: read inventory.h and main.c before class. Both appear in Section 4 so you can read them here.
  • Keep the Defensive Programming reference card (Section 4) open while you work. It is the one-page summary you will reuse well beyond this lesson.
3. Pre-Class Work

Pre-Class Checklist

  • Read the Lesson 37 supplementary notes. Short. Frames the four pieces and the one rule that ties them together.
  • Read inventory.h and main.c (Section 4). Come to class able to say, in one sentence each, what the loader functions promise and what main does with a failed line.
  • Skim the In-Class section below. Know what the two widgets and the lab will ask of you.
4. Lesson Materials and Overview

This lesson is a synthesis. Each piece below is something you have touched before; the goal today is to use them together and to apply them to code you did not write.

Jump to
Slides Reading Existing Code The Contract The Driver Assertions Error Codes & errno Input Validation Reference Card

Slides

The Lesson 37 deck walks through reading existing code, the assert-vs-validate distinction, error reporting with errno, and input validation, then sets up the lab.

Open Lesson 37 slides →

Reading code you did not write

Most code you will work on already exists, and reading it is its own skill. Find the entry point and follow the data: where input enters, what transforms it, where it ends up. Read the contract (the header) before the implementation. Then do the thing that finds bugs: trace the inputs the author did not consider, such as a NULL pointer, an empty line, a value out of range, a missing field. Working code handles the cases its author imagined; the gaps are everything else.

The contract: inventory.h

This header is the spec for the lab. Read each prototype as a promise: what it takes, what it returns, and what it sets errno to on failure. You will match these promises exactly when you harden the loader.

#ifndef INVENTORY_H
#define INVENTORY_H

/*
 * inventory.h - shared contract for the marketplace inventory loader.
 *
 * PROVIDED FILE. Do not modify. The autograder and main.c depend on
 * these declarations exactly as written.
 */

/* Longest item name we store, including the terminating '\0'. */
#define NAME_MAX_LEN 64

/* A quantity above this is treated as bad data, not real stock. */
#define MAX_QTY 10000

/* Capacity of the in-memory inventory array. */
#define CAPACITY 256

/* One parsed marketplace listing. */
struct listing {
    char name[NAME_MAX_LEN];
    int price;          /* whole dollars; must be >= 0 */
    int quantity;       /* must be 0..MAX_QTY inclusive */
    char condition;     /* 'N' new, 'U' used, or 'R' refurbished */
};

/*
 * load_listing - parse one CSV line "name,price,quantity,condition"
 *                into *out.
 *
 * @param line  the raw input line (may be NULL)
 * @param out   destination listing (may be NULL)
 * @return 0 on success.
 *         -1 on failure, with errno set:
 *            EFAULT if line or out is NULL,
 *            EINVAL if the line is malformed or any field is out of range.
 *
 * On failure *out is left unchanged.
 */
int load_listing(const char *line, struct listing *out);

/*
 * add_listing - append item to inventory, tracking the running count.
 *
 * @param inventory  the array, with room for CAPACITY entries
 * @param count      pointer to the current number of stored items
 * @param item       the listing to append (must be non-NULL)
 * @return 0 on success (item stored, *count incremented).
 *         -1 if the inventory is full, with errno set to ENOSPC.
 *
 * It is a programmer error (not a runtime condition) for count to be
 * NULL or *count to be negative; those are guarded with assert.
 */
int add_listing(struct listing inventory[], int *count,
                const struct listing *item);

#endif /* INVENTORY_H */

The driver: main.c

This is the code that calls your functions. You cannot harden load_listing well without understanding how it is used. Notice how main reads one line at a time, what it does when a line is rejected, and which stream the error message goes to.

/*
 * main.c - driver for the marketplace inventory loader.
 *
 * PROVIDED FILE. Do not modify. Read it carefully: this is the code that
 * calls your functions, and you cannot harden load_listing well without
 * understanding how it is used.
 *
 * Usage:
 *   ./inventory < feed.txt
 *
 * Reads one listing per line from standard input. Valid listings are
 * stored; invalid lines are reported to stderr (with errno explained)
 * and skipped. At the end it prints the accepted inventory.
 */

#include <errno.h>
#include <stdio.h>
#include <string.h>

#include "inventory.h"

#define LINE_BUF 256

static const char *condition_word(char c)
{
    switch (c) {
        case 'N': return "new";
        case 'U': return "used";
        case 'R': return "refurbished";
        default:  return "unknown";
    }
}

int main(void)
{
    struct listing inventory[CAPACITY];
    int count = 0;
    char line[LINE_BUF];
    int line_no = 0;

    while (fgets(line, sizeof line, stdin) != NULL) {
        line_no++;

        /* Strip a trailing newline so condition is the last real char. */
        size_t len = strlen(line);
        if (len > 0 && line[len - 1] == '\n') {
            line[len - 1] = '\0';
        }

        /* Skip blank lines silently. */
        if (line[0] == '\0') {
            continue;
        }

        struct listing item;
        errno = 0;
        if (load_listing(line, &item) != 0) {
            /* perror turns the current errno into a readable reason and
               writes it to stderr, not stdout. */
            fprintf(stderr, "line %d: rejected: ", line_no);
            perror(line);
            continue;
        }

        if (add_listing(inventory, &count, &item) != 0) {
            fprintf(stderr, "line %d: not stored: ", line_no);
            perror("inventory");
            continue;
        }
    }

    printf("Accepted %d listing(s):\n", count);
    for (int i = 0; i < count; i++) {
        printf("  %-20s $%d  qty %d  (%s)\n",
               inventory[i].name,
               inventory[i].price,
               inventory[i].quantity,
               condition_word(inventory[i].condition));
    }

    return 0;
}

Assertions: catching your own bugs

assert(condition); from <assert.h> aborts with a file-and-line message if the condition is false. Use it for invariants, conditions that can only be false if your own logic has a bug: an index your code computed being in bounds, a count never going negative. The catch is that -DNDEBUG compiles every assertion out of release builds, so an assertion is a development-time check, never a runtime guarantee. That is the reason you never validate untrusted input with an assertion.

Error codes and errno: failing on purpose

When a function cannot do its job, it should fail in a way the caller can detect and explain. The common C pattern is to return a status (often 0 success, -1 failure) and set errno from <errno.h> to say why: EINVAL for invalid data, EFAULT for a bad address such as NULL, ENOSPC when there is no room. The caller turns that into text with perror or strerror(errno) and writes it to stderr, so error messages never get mixed into the program's real output.

Input validation: handling the world

Validation is the runtime handling of data you cannot trust, and the order rarely changes: guard pointers before dereferencing; check return values (sscanf returns how many fields it filled); range-check every field on both ends; allow-list enumerated values and reject everything else; and commit to the output only after every check passes, so a rejected input never leaves a half-filled result. The whole lesson reduces to one rule: assertions for your bugs, validation for bad input.

Defensive Programming reference card

Keep this open while you code. It is cross-cutting: you will see it referenced from earlier defensive-programming touchpoints, and it lives here at its home lesson.

CS210 · Reference Card

Defensive Programming

Assertions · Error codes & errno · Input validation · One page, keep it open while you code.
The one rule that organizes everything: use assertions for conditions that are false only when your own code has a bug; use validation for bad data that is an expected runtime event. Assertions catch programmer error; validation handles the world.

Assertions — #include <assert.h>

  • assert(condition); aborts the program with a file and line message if condition is false.
  • Use it to state and check invariants: things that must be true if your logic is correct (a computed index in bounds, a count never negative, a pointer your own code just set).
  • Compiled out by -DNDEBUG. Release builds delete every assert. Never put anything you always need to happen inside an assert.
  • Never assert on input you do not control (user, file, network). That is validation's job.
assert(count >= 0);          /* my bug if false  -> assert  */
assert(index < CAPACITY);    /* my bug if false  -> assert  */

Error codes & errno — #include <errno.h>

  • Return a value that tells the caller success or failure (a common C convention is 0 for success, -1 for failure).
  • errno is a shared global the standard library sets on failure; your code can set it too. Pair it with your return so the caller learns that it failed and why.
  • Common values: EINVAL (invalid argument/data), EFAULT (bad address / NULL), ENOSPC (no space left).
  • Report with perror("context") or strerror(errno). Send error text to stderr, not stdout.
  • Set errno = 0; before a call when you intend to inspect it afterward.
if (bad_data) { errno = EINVAL; return -1; }
...
if (load_listing(line, &item) != 0)
    perror("load_listing");   /* writes reason to stderr */

Input validation

  • Guard pointers first. Check for NULL before dereferencing anything.
  • Check return values. scanf/sscanf return how many fields were assigned; anything short means the rest are garbage.
  • Range-check every field, both ends. A value can parse cleanly and still be nonsense.
  • Allow-list enumerated inputs. Accept the known-good set; reject everything else. Fail closed, not open.
  • Commit after validating. Write to the output only once every check passes, so a rejected input never leaves a half-filled result.

Reading code you did not write

  • Find the entry point and follow the data: where does input come from, where does it go.
  • Read the header/contract first; it tells you what each function promises before you read how it delivers.
  • Trace the inputs the author did not consider: NULL, empty, too long, out of range, wrong type. That is where the bugs live.
  • Watch for = where == was meant, ignored return values, and missing bounds checks.
5. In-Class Work

Three things to do in class. Use the checklist to track your progress.

  • Work through the Harden the Loader walkthrough below. Thirteen guided tasks: read a fragile loader, then add one defensive guard at a time and watch the bad records get caught. About 25 minutes.
  • Take the Assert vs Validate self-check. Seven questions on the line between catching your bugs and handling bad input. About 10 minutes.
  • Complete Lab 37: Harden the Marketplace Inventory Loader. One lab, 2 points. Auto-graded through GitHub Classroom. Full instructions in the lab repo.

Harden the Loader Walkthrough

Thirteen tasks across five sections. Read the fragile loader, then type each guard at the prompt. The right pane shows the loader's behavior on a fixed test feed and the guards you have installed; both update as you go.

Before you begin

Enter your name. It will appear on the completion screen so you can include it in your screenshot for the Lesson 37 reflection.

CS210 Lesson 37 · Harden the Loader

You inherited a marketplace inventory loader that someone else wrote. It works on clean data and falls apart on everything else. Read it, then add one guard at a time. The right pane shows what the loader does to a fixed test feed; watch the bad records get caught as you go.
Task 0 of 13
Section 1: Read before you change
Current task
Loading...
Loader behavior on the test feed
Guards installed
harden>
How this works: This is not a compiler. It is a teaching tool that simulates how the loader behaves on a fixed feed of records as you add defensive checks. The task card tells you what to add next; only the expected guard advances the walkthrough so the behavior stays predictable. Some tasks ask you to predict or decide instead of type a guard — read those carefully. Mistype twice and the hint button will pulse.

Assert vs Validate Self-Check

Seven questions. Pick or type your answer and submit; answers lock once submitted. Pass at 70% or above.

Before you begin

Enter your name. It will appear on the score card so you can include it in your screenshot for the Blackboard reflection.

CS210 Lesson 37 · Assert vs Validate

Seven questions on the line between catching your own bugs and handling bad input: assertions, validation, errno, and reporting. Pass at 70% or above; below that, take it again after reviewing.
How this works: One question per card. Pick or type your answer and click Submit. Feedback appears, then click Next to move on. Answers lock once submitted, so think before you submit. After all seven, you get a score, a completion code, and a per-question recap.
6. After Class
  • Finish Lab 37 if you did not complete it in class. Submit through GitHub Classroom and confirm the autograder ran on Gradescope.
  • Complete the Lesson 37 reflection in Blackboard. Enter both widget completion codes and answer the reflection prompt; attach your screenshot of the walkthrough completion screen.
  • Preview the Lesson 38 materials on program memory layout: where code, globals, the heap, and the stack actually live. The last task of the walkthrough was your first landmark.
  • Lessons 37 and 38 close out Block 5. Keep the Defensive Programming reference card handy; it applies to every program you write from here on.
Need help? Schedule EI with your instructor. Bring the specific input that breaks your loader, the exact error message or output, and the guard you think should have caught it. Specific questions get unstuck quickly.