Secure Coding in C · beginner · ~12 min

Score a password against a policy (defensive)

- Walk a NUL-terminated string one character at a time and classify each byte as uppercase, lowercase, digit, or special. - Track the four character-class flags and the length in a single pass, without reading past the terminator. - Represent a set of rules as a small configuration `struct` (a password policy) instead of hard-coding limits. - Report *every* failed requirement at once by returning a bitmask, where each bit stands for one rule. - Use the `<ctype.h>` classification functions safely (casting through `unsigned char`) and know when to avoid them. - Understand why a password policy is a cheap, high-value defensive layer, and where its limits are.

Overview

A password policy validator answers one narrow question: does this candidate password obey the rules we set? Rules are things like "at least 12 characters", "must contain an uppercase letter", "must contain a digit", and "must contain a special character". The validator does not store the password, hash it, or send it anywhere. It just reads the characters and decides pass or fail.

This builds directly on C strings. A password arrives as a const char * — a pointer to the first byte of a NUL-terminated array of characters. Everything you learned there applies: the string ends at the '\0' byte, you must never read past it, and you can walk it with a pointer or an index. Here we walk it once, and while walking we tally what we see.

In plain language: imagine reading a word letter by letter and ticking boxes on a checklist — "saw a capital letter", "saw a number", "saw a symbol". At the end you look at your checklist and the total length, compare them to what the policy demands, and hand back a report of anything missing. In C terminology, the checklist boxes are boolean flags, the report is a bitmask (one integer whose individual bits carry yes/no answers), and the character tests come from the standard <ctype.h> header.

This is the defensive counterpart to attacks like brute forcing. Instead of trying to crack passwords, you make weak passwords impossible to register in the first place.

Why it matters

A password policy is one of the cheapest and most effective defensive layers in any authentication system. A dozen lines of validation, enforced at sign-up and at every password change, quietly eliminate the weakest passwords — the password, 123456, and qwerty entries that dominate every leaked-credential dump and that automated brute-force and credential-stuffing tools try first.

The skill generalises far beyond passwords. "Read untrusted input once, classify each byte, and decide whether it is acceptable" is the shape of nearly all input validation: checking usernames, sanitising filenames, verifying that a field is numeric, rejecting control characters in a log line. Getting the string-walking and the pass/fail reporting right here is practice you will reuse constantly.

Returning a bitmask of every failure rather than a single yes/no also matters for real products. A good sign-up form tells the user "needs an uppercase letter and a digit" in one shot instead of making them guess one rule at a time. The bitmask is how the validation core communicates that full picture to the user interface.

Core concepts

1. The password as a NUL-terminated string

Definition. The password is a const char * — the address of the first char in an array that ends with a '\0' (NUL) byte. const promises the function will not modify it.

Plain-language explanation. You do not get told the length up front. You discover the end by reading forward until you hit the '\0'. Every byte before it is a real password character.

How it works internally. In memory a password like "Ab3!" looks like this:

 address:  0x100  0x101  0x102  0x103  0x104
 value  :  'A'    'b'    '3'    '!'    '\0'
 index  :   0      1      2      3      4
 pw ---->  first byte;  loop stops when *pw == '\0'

The five bytes are contiguous. pw[0] is 'A', pw[4] is the terminator. Reading pw[5] would be out of bounds.

When to use / not use. Walking with an index (for (size_t i = 0; pw[i]; i++)) is the natural tool here. Do not call strlen first and then loop a second time — that reads the whole string twice for no reason. One pass does everything.

Pitfall. Passing NULL instead of a real string. pw[0] on a NULL pointer is undefined behaviour and usually crashes. Check if (pw == NULL) at the top.

Knowledge check. For the string "pw", how many bytes are stored in memory, and what is the value of the last one?

2. Character classification with <ctype.h>

Definition. <ctype.h> provides isupper, islower, isdigit, isalnum, and friends. Each takes an int and returns non-zero (true) if the character belongs to that class.

Plain-language explanation. Instead of writing c >= 'A' && c <= 'Z' yourself, you call isupper(c). It reads better and handles the classes for you. A "special" character here means not a letter and not a digit — i.e. !isalnum(c).

How it works internally. These functions look the character up in a small table. The catch: their argument must be representable as an unsigned char or equal EOF. A plain char can be signed, so a byte like 0xE9 becomes a negative int, which is undefined behaviour if passed directly. The fix is to cast: isupper((unsigned char)c).

When to use / not use. Use <ctype.h> for readable, correct ASCII classification. Do not rely on it for full Unicode — it classifies one byte at a time and knows nothing about multibyte UTF-8 sequences.

Pitfall. isdigit(c) without the cast, on a string containing high-bit bytes, is a real, subtle bug. Always cast through unsigned char.

Function True when the character is... Example true Example false
isupper(c) an uppercase letter 'K' 'k', '7'
islower(c) a lowercase letter 'k' 'K', '!'
isdigit(c) a decimal digit 09 '7' 'x', ' '
isalnum(c) a letter or digit 'k', '7' '!', ' '
!isalnum(c) a "special" (our definition) '!', ' ' 'k', '7'

Knowledge check (predict the output). What does isalnum((unsigned char)'#') return, roughly, and is '#' a "special" character under our rule?

3. Single-pass flag tallying

Definition. You keep four int flags — has_upper, has_lower, has_digit, has_special — all starting at 0, and set the relevant one to 1 the moment you see a matching character.

Plain-language explanation. Think of four light switches, all off. As you scan, the first uppercase letter flips has_upper on; it stays on. At the end, an off switch means "never saw one of those".

How it works internally. One for loop over the bytes. For each character you run a short chain of if tests and flip at most one flag. Length is just a counter you increment each iteration.

 scan "aB3"
 ------------------------------------------
 char  has_upper has_lower has_digit has_special  len
 start     0         0         0          0        0
 'a'       0         1         0          0        1
 'B'       1         1         0          0        2
 '3'       1         1         1          0        3
 ------------------------------------------
 result: upper=yes lower=yes digit=yes special=no

When to use / not use. Perfect when the rules are independent "contains at least one of X" checks. Not the right tool if a rule needs counts (e.g. "at least two digits") — then use integer counters instead of 0/1 flags.

Pitfall. Resetting a flag inside the loop (has_upper = isupper(c)). That overwrites a previous 1 with 0 on the next non-uppercase character. Only ever set flags to 1; never back to 0 mid-scan.

Knowledge check (find the bug). A learner writes has_digit = isdigit((unsigned char)c) ? 1 : 0; inside the loop. Why does "a1b" end with has_digit == 0?

4. The policy struct

Definition. A pw_policy_t is a small struct holding the numbers and booleans that describe the rules: minimum length and which classes are required.

Plain-language explanation. Instead of baking 12 and "require a digit" into the code, you put them in data. Different apps can then enforce different policies with the same function.

How it works internally. The struct is passed by pointer (const pw_policy_t *p) so you read fields as p->min_length, p->require_digit, and so on. Passing by pointer avoids copying the struct and the const guarantees the validator will not alter it.

When to use / not use. Use a config struct whenever the same logic must serve multiple rule sets. For a single hard-wired policy in a tiny program, plain constants are fine — but the struct is the professional habit.

Pitfall. Forgetting that a require_* field of 0 means "not required". You must only fail a class check when the policy requires it: if (p->require_upper && !has_upper) fail....

5. The bitmask result

Definition. A bitmask is a single integer used as a bag of yes/no flags. Each named bit stands for one requirement; a set bit means that requirement failed.

Plain-language explanation. Rather than return one boolean ("bad"), you return an integer where bit 0 might mean "too short", bit 1 "no uppercase", bit 2 "no lowercase", and so on. The caller inspects the bits to show precise messages. 0 means every bit clear — a clean pass.

How it works internally. You define one bit per rule with the left-shift operator, combine failures with bitwise OR (|=), and test with bitwise AND (&).

 bit index :   4        3        2         1          0
 meaning   : special  digit   lower    upper     too-short
 value     :  1<<4     1<<3    1<<2     1<<1       1<<0

 example: password too short AND missing a digit
   fails  =  (1<<0) | (1<<3)  =  0b01001  =  9
   test "missing digit?":  fails & (1<<3)  ->  non-zero  ->  yes

When to use / not use. Bitmasks shine when you have a small fixed set of independent flags and want them in one return value. Do not overuse them for large or dynamic sets — a struct of named booleans is clearer past a handful of flags.

Pitfall. Testing with == instead of &. fails == PW_NO_DIGIT is only true when the only failure is the digit; fails & PW_NO_DIGIT correctly detects the digit failure even when other bits are also set.

Knowledge check (explain in your own words). Why does returning a bitmask let a sign-up form list all password problems at once, while returning 1 for any failure cannot?

Syntax notes

Named bit constants, the policy struct, and the core tests:

#include <ctype.h>   // isupper, islower, isdigit, isalnum
#include <stddef.h>  // size_t, NULL

/* One bit per rule. Left-shift makes each a distinct power of two. */
#define PW_TOO_SHORT  (1u << 0)   /* 0b00001 */
#define PW_NO_UPPER   (1u << 1)   /* 0b00010 */
#define PW_NO_LOWER   (1u << 2)   /* 0b00100 */
#define PW_NO_DIGIT   (1u << 3)   /* 0b01000 */
#define PW_NO_SPECIAL (1u << 4)   /* 0b10000 */

typedef struct {
    int min_length;       /* e.g. 12 */
    int require_upper;    /* boolean: needs an uppercase letter? */
    int require_lower;    /* boolean: needs a lowercase letter?  */
    int require_digit;    /* boolean: needs a digit?            */
    int require_special;  /* boolean: needs a special char?     */
} pw_policy_t;

/* classify one byte — always cast through unsigned char */
unsigned char c = (unsigned char)pw[i];
if (isupper(c)) has_upper = 1;          /* set, never clear */

/* record a failure by OR-ing its bit in */
unsigned int fails = 0u;
if (p->require_digit && !has_digit) fails |= PW_NO_DIGIT;

/* the caller tests a single failure with AND */
if (fails & PW_NO_DIGIT) puts("add a digit");

Key points: each PW_* constant is a distinct power of two so they never collide; |= accumulates failures; & bit tests one; a return of 0 means "no bits set" = pass.

Lesson

Why this matters

The defensive answer to "how do we stop brute-force?" is rarely "build a faster password cracker."

It is "enforce a sensible password policy at registration and password-change time."

This exercise teaches the validator side: given a candidate password and a policy, decide whether the password is acceptable.

No hashing. No cracking. No online tests. This is pure string and character-class logic.

What the policy looks like

The policy is a small struct describing the rules to enforce:

typedef struct {
    int min_length;          // e.g. 12
    int require_upper;       // boolean: needs an uppercase letter?
    int require_lower;       // boolean: needs a lowercase letter?
    int require_digit;       // boolean: needs a digit?
    int require_special;     // boolean: needs a special character?
} pw_policy_t;

Your job

Implement this function:

int pw_check(const char *pw, const pw_policy_t *p);

It should return:

  • 0 — the password meets all required classes and the length requirement.
  • non-zero — the password fails one or more requirements.

For the non-zero case, each bit position represents which requirement failed. OR the failing bits together so the caller can see every problem at once.

Common mistakes

  • Reading past the NUL terminator. Use strnlen(pw, MAX) if you want a hard cap on how far you read.
  • Worrying about locale. The ranges 'A'..'Z' and 'a'..'z' are locale-independent in C when you cast through unsigned char. That is fine here.
  • Returning 1 for every failure. The test harness expects distinct bits, so the caller can tell which class is missing.

Code examples

#include <stdio.h> #include <ctype.h> #include <string.h> #include <stddef.h>

/* One bit per failed requirement. */ #define PW_TOO_SHORT (1u << 0) #define PW_NO_UPPER (1u << 1) #define PW_NO_LOWER (1u << 2) #define PW_NO_DIGIT (1u << 3) #define PW_NO_SPECIAL (1u << 4)

/* Hard cap so a missing terminator can never make us read forever. */ #define PW_MAX 256

typedef struct { int min_length; int require_upper; int require_lower; int require_digit; int require_special; } pw_policy_t;

/* Return 0 if pw satisfies p; otherwise a bitmask of failed rules. */ unsigned int pw_check(const char *pw, const pw_policy_t p) { if (pw == NULL || p == NULL) return PW_TOO_SHORT; / treat missing input as failing */

int has_upper = 0, has_lower = 0, has_digit = 0, has_special = 0;

/* strnlen never scans past PW_MAX bytes, even without a terminator. */
size_t len = strnlen(pw, PW_MAX);

for (size_t i = 0; i < len; i++) {
    unsigned char c = (unsigned char)pw[i];   /* safe for <ctype.h> */
    if (isupper(c))      has_upper = 1;
    else if (islower(c)) has_lower = 1;
    else if (isdigit(c)) has_digit = 1;
    else                 has_special = 1;     /* not a letter/digit */
}

unsigned int fails = 0u;
if ((int)len < p->min_length)             fails |= PW_TOO_SHORT;
if (p->require_upper   && !has_upper)     fails |= PW_NO_UPPER;
if (p->require_lower   && !has_lower)     fails |= PW_NO_LOWER;
if (p->require_digit   && !has_digit)     fails |= PW_NO_DIGIT;
if (p->require_special && !has_special)   fails |= PW_NO_SPECIAL;
return fails;

}

/* Print a human-readable report for one password. */ static void report(const char *pw, const pw_policy_t *p) { unsigned int f = pw_check(pw, p); printf(""%s" -> mask %u : ", pw, f); if (f == 0) { printf("OK\n"); return; } if (f & PW_TOO_SHORT) printf("[too short] "); if (f & PW_NO_UPPER) printf("[no upper] "); if (f & PW_NO_LOWER) printf("[no lower] "); if (f & PW_NO_DIGIT) printf("[no digit] "); if (f & PW_NO_SPECIAL) printf("[no special] "); printf("\n"); }

int main(void) { pw_policy_t policy = { .min_length = 12, .require_upper = 1, .require_lower = 1, .require_digit = 1, .require_special = 1, };

report("password",           &policy);   /* too short, no upper/digit/special */
report("Sh0rt!",             &policy);   /* only too short */
report("CorrectHorse42!",    &policy);   /* passes every rule */
report("alllowercaseletters",&policy);   /* long enough, missing classes */
return 0;

}

Line by line

Setup. The PW_* macros give each rule a distinct bit. PW_MAX (256) is a safety ceiling on how far we ever read. pw_policy_t bundles the rules; main fills one with min length 12 and all four classes required, using designated initialisers (.min_length = 12).

Entering pw_check. First the guard: if pw or p is NULL, return a non-zero mask instead of dereferencing a null pointer. The four flags start at 0 ("not seen yet"). strnlen(pw, PW_MAX) gives the length but stops at 256 bytes even if the terminator is missing — a defensive substitute for strlen.

The scan loop. For each index i, we copy the byte into an unsigned char c — the cast that makes the <ctype.h> calls well-defined. The if / else if chain flips exactly one flag: uppercase → has_upper, lowercase → has_lower, digit → has_digit, anything else → has_special. Flags are only ever set to 1, so once true they stay true.

Trace for "Sh0rt!" (length 6, below the min of 12):

i pw[i] class flag set flags after (U,L,D,S)
0 S upper has_upper 1,0,0,0
1 h lower has_lower 1,1,0,0
2 0 digit has_digit 1,1,1,0
3 r lower (already) 1,1,1,0
4 t lower (already) 1,1,1,0
5 ! special has_special 1,1,1,1

Building the mask. After the loop all four flags are 1, so no class bits are set. But len is 6 and p->min_length is 12, so (int)len < p->min_length is true and fails |= PW_TOO_SHORT sets bit 0. The function returns 1 (0b00001).

Reporting. report prints the raw mask then, using f & bit tests, the matching labels. For "Sh0rt!" only [too short] prints. For "CorrectHorse42!" every flag is set and the length is 15, so fails stays 0 and report prints OK. For "password" the mask is PW_TOO_SHORT | PW_NO_UPPER | PW_NO_DIGIT | PW_NO_SPECIAL = 1 | 2 | 8 | 16 = 27.

Expected output:

"password" -> mask 27 : [too short] [no upper] [no digit] [no special] 
"Sh0rt!" -> mask 1 : [too short] 
"CorrectHorse42!" -> mask 0 : OK
"alllowercaseletters" -> mask 26 : [no upper] [no digit] [no special] 

Edge cases: the empty string "" gives length 0 and no flags, so it fails on PW_TOO_SHORT plus every required class. A password of exactly min_length passes the length test (the comparison is strictly <).

Common mistakes

Mistake 1 — passing a plain char to isupper.

char c = pw[i];
if (isupper(c)) ...        /* WRONG: char may be signed */

Why it is wrong: on platforms where char is signed, a byte ≥ 0x80 becomes a negative int, and passing a negative value (other than EOF) to isupper is undefined behaviour. Fix: cast through unsigned char.

unsigned char c = (unsigned char)pw[i];
if (isupper(c)) ...        /* correct */

Recognise it: crashes or garbage classification only on inputs with high-bit bytes. Prevent it by always casting.

Mistake 2 — clearing a flag inside the loop.

has_digit = isdigit((unsigned char)pw[i]) ? 1 : 0;   /* WRONG */

Why: this assigns every iteration, so a later non-digit resets a previously found 1 back to 0. "a1b" ends with has_digit == 0. Fix: only ever set to 1:

if (isdigit((unsigned char)pw[i])) has_digit = 1;

Mistake 3 — testing the mask with == instead of &.

if (fails == PW_NO_DIGIT) puts("add a digit");   /* WRONG when other bits set */

Why: == is only true when the digit failure is the only failure. If the password is also too short, the digit message never prints. Fix: if (fails & PW_NO_DIGIT).

Mistake 4 — collapsing all failures into a single value.

if (!has_digit) return 1;
if (!has_upper) return 1;   /* WRONG: caller cannot tell which */

Why: returning 1 everywhere throws away which rule failed, so the UI can only say "invalid password". Fix: OR distinct bits and return the accumulated mask.

Mistake 5 — using < vs <= wrong for length. Deciding "too short" with len <= p->min_length rejects a password of exactly the minimum. The requirement is "at least min_length", so the failing condition is strictly len < p->min_length.

Debugging tips

Compiler errors.

  • implicit declaration of isupper/strnlen — you forgot #include <ctype.h> or #include <string.h>. (If strnlen is unavailable on your toolchain, compute the length with a bounded loop instead.)
  • pw_policy_t undeclared — the typedef struct { ... } pw_policy_t; must appear before the function that uses it.
  • comparison between signed and unsigned warning on len < p->min_length — cast: (int)len < p->min_length, or make min_length a size_t.

Runtime errors.

  • Segfault on the first character usually means pw was NULL or not terminated. Add the NULL guard and use strnlen(pw, PW_MAX).
  • Reading forever / huge length means the string was never NUL-terminated; the PW_MAX cap in strnlen prevents the runaway.

Logic errors.

  • Everything reports as failing → check you initialised the flags to 0 and are setting them to 1 (not the reverse).
  • A valid password reports a missing class → you probably reset a flag mid-loop (Mistake 2) or your else if chain has a gap.
  • Correct passwords marked too short → off-by-one in the length comparison (<= vs <).

Questions to ask when it misbehaves.

  1. Print the raw mask value and convert it to binary — which bits are set versus which you expected?
  2. Add a temporary printf of has_upper..has_special and len right after the loop.
  3. Feed known inputs: "", a 12-char all-lowercase string, and one that satisfies every rule. Do the masks match by hand?
  4. Are you testing failures with & (correct) or == (a common trap)?

Memory safety

Reading past the buffer. The single greatest risk here is walking off the end of the string. strlen/an index loop assume a '\0' exists; a malformed buffer without one makes them read arbitrary memory (a buffer over-read — the class of bug behind Heartbleed). Defensive fix: bound every scan with strnlen(pw, PW_MAX) or an explicit i < PW_MAX guard, so you can never read more than a known number of bytes.

Null and uninitialised pointers. Dereferencing NULL or an uninitialised pw is undefined behaviour. Guard both pw and the policy pointer at the top and treat missing input as failing validation, never as passing.

Undefined behaviour in <ctype.h>. As covered above, passing a possibly-negative char to isupper/isdigit/isalnum is undefined. Always cast through unsigned char. This is a genuine, standard-mandated hazard, not a style preference.

Signed/unsigned length. strnlen returns size_t (unsigned). Comparing it to a signed min_length can warn or, in edge cases, mislead; cast deliberately so the comparison means what you intend.

Defensive security posture.

  • Validate, do not store. This function reads a password and returns a verdict. It must not log the password, copy it into a global, or keep it around after returning. Passwords in memory should be treated as sensitive and their lifetime kept short.
  • Least surprise, fail closed. On any uncertainty (NULL, over-length, empty) return a failing mask, never a pass.
  • Policy is only one layer. Composition rules stop the very weakest passwords but do not stop a user picking Password123! (which passes every classic rule yet appears in breach lists). Modern guidance (NIST SP 800-63B) favours a generous length minimum plus a check against known-breached passwords over rigid composition rules. Enforce length here; pair it with a breach-list lookup in a real system. Everything in this lesson is local string logic — no hashing, no network, no live targets.

Real-world uses

Where this runs in real systems. Every sign-up and change-password endpoint runs a validator like this before accepting a new credential: web frameworks (the password rules in Django, Rails, ASP.NET Identity), operating systems (Linux PAM's pam_pwquality/pam_cracklib modules enforce length and class rules at passwd time), password managers giving strength feedback, and embedded devices forcing you off the default admin password. The same one-pass "classify each byte, decide pass/fail" pattern also drives username validation, filename sanitisation, and rejecting control characters in log output.

Best-practice habits.

Beginner:

  • Give bits and flags descriptive names (PW_NO_DIGIT, has_upper), not magic numbers.
  • Always cast to unsigned char before <ctype.h> calls, every time.
  • Guard NULL inputs and fail closed.
  • Bound your scan (strnlen) so a bad string cannot run you off the buffer.
  • Test the boundary inputs: empty, exactly min_length, all-one-class, and a fully valid password.

Advanced:

  • Make the policy fully data-driven (the struct) so one function serves many products, and keep the validation core free of I/O so it is trivially unit-testable.
  • Return the full failure bitmask so the UI can report every problem at once and localise the messages.
  • Follow current guidance: emphasise length, screen against a breached-password list, and avoid forcing periodic rotation — composition rules alone are a weak signal.
  • Handle secrets carefully: minimise how long the plaintext lives in memory and never log it.

Practice tasks

Beginner 1 — Count the classes. Write void classify(const char *pw) that prints how many uppercase letters, lowercase letters, digits, and special characters a password contains. Requirements: one pass, cast through unsigned char, treat "special" as !isalnum. Example: input "Ab3!" prints upper=1 lower=1 digit=1 special=1. Concepts: string walking, <ctype.h>, counters.

Beginner 2 — Has a special character? Implement int has_special(const char *pw) returning 1 if pw contains at least one non-alphanumeric character, else 0. Requirements: return early once found; handle the empty string (returns 0) and NULL (returns 0). Example: "abc123"0, "abc 123"1. Concepts: single-pass scan, isalnum, early exit.

Intermediate 1 — Minimal pw_check. Implement unsigned int pw_check(const char *pw, const pw_policy_t *p) matching the lesson: length check plus the four class flags, returning a bitmask with the PW_* bit meanings. Requirements: NULL guard, bounded scan, only set flags to 1. Input/output: with min length 8 and all classes required, "aB3!aB3!"0; "aaaaaaaa"PW_NO_UPPER | PW_NO_DIGIT | PW_NO_SPECIAL. Concepts: flags, bitmask, policy struct.

Intermediate 2 — Reject whitespace and report it. Extend pw_check with a new rule PW_HAS_SPACE that fails when the password contains any character where isspace is true. Requirements: add the bit without disturbing the existing ones, and update the reporter to print [has space]. Example: "Ab3! xyz9" sets PW_HAS_SPACE. Constraints: still one pass. Concepts: extending a bitmask, isspace.

Challenge — Count-based rules and a strength score. Change the policy so each class requirement is a minimum count (min_upper, min_digit, ...) instead of a boolean, and add a function int pw_strength(const char *pw, const pw_policy_t *p) returning a 0–100 score based on length and class variety (define your own reasonable formula). Requirements: keep the count logic in one pass, document your scoring, guard against integer overflow on absurdly long inputs (cap with PW_MAX), and print both the failure mask and the score. Hint: replace 0/1 flags with int counters and compare each against its minimum. Concepts: counters vs flags, policy design, defensive bounds, reporting.

Summary

A password policy validator reads a candidate password once, classifies each byte, and decides whether it obeys the rules — no hashing, no network, pure local string logic. It is one of the cheapest, highest-value defensive layers in authentication and a template for input validation in general.

Core ideas. Walk the NUL-terminated string a single time; for each byte, cast to unsigned char and use <ctype.h> (isupper, islower, isdigit, isalnum) to set one of four flags — only ever setting flags to 1, never clearing them. Track length as you go. Keep the rules in a pw_policy_t struct so one function serves many policies. Report results as a bitmask: one bit per rule, OR failures together with |=, test them with &, and let 0 mean a clean pass so the caller can list every problem at once.

Most important syntax. unsigned char c = (unsigned char)pw[i]; before any <ctype.h> call; #define PW_NO_DIGIT (1u << 3) for distinct bits; fails |= PW_NO_DIGIT; to record; if (fails & PW_NO_DIGIT) to test.

Common mistakes. Passing a signed char to isupper (undefined behaviour); clearing a flag mid-loop; testing the mask with == instead of &; collapsing all failures into a single 1; off-by-one on the length check; reading past a missing terminator.

Remember. Guard NULL, bound your scan with strnlen, fail closed, and know the limit: composition rules stop only the weakest passwords — real systems pair a generous length minimum with a breached-password check.

Practice with these exercises