Semana 10 — Cadenas
Objetivos
- Pasar arreglos a funciones y explicar por qué la función puede cambiarlos.
- Explicar qué es una cadena en C y recorrerla.
- Reconocer y evitar los buffer overflows.
Teoría — Arreglos y funciones · Cadenas
Lectura: Apuntes, semana 10.
Temas:
- Pasar un arreglo a una función: se pasa su dirección, y el tamaño va aparte.
- Mantenimiento de un vector: insertar y eliminar moviendo elementos.
- Cadena: un arreglo de
charterminado en\0. Dibujo en memoria. - Recorrer una cadena hasta
\0. Escribirmy_strlen. - Leer texto de forma segura con
fgetsy límites enscanf. - Error común: olvidar el espacio para
\0. Buffer overflow y por qué es un problema de seguridad.
Laboratorio — Cadenas y errores de memoria
- Escribir sin
string.hlas funcionesmy_strlen,my_strcmpyto_uppercon pruebasassert. - Escribir
is_palindrome, que compara bytes exactos, eis_palindrome_text, que ignora espacios y mayúsculas. - Provocar un overflow con un buffer pequeño. Leer el informe de
-fsanitize=addressy corregirlo confgets.
Preguntas de repaso
- Dibujen en memoria
char s[8] = "hola";. ¿Cuánto valenmy_strlen(s)ysizeof(s)? - ¿Por qué una función puede cambiar un arreglo recibido, pero no un
int? - ¿Por qué
sizeofno da el tamaño del arreglo dentro de la función? - ¿Por qué es peligroso
scanf("%s", s)? ¿Cómo se corrige? - ¿Por qué
my_strlen("año")vale 4?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (15)Errores (9)Pruebas (3)Laboratorio (2)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
// Primero scanf y después fgets: hay que descartar el resto de la línea del número.
#include <stdio.h>
#define SIZE 20
void discard_rest(void) {
int c = getchar();
while (c != '\n' && c != EOF) {
c = getchar();
}
}
int main(void) {
int age;
char name[SIZE];
if (scanf("%d", &age) != 1) {
printf("Entrada inválida\n");
return 1;
}
discard_rest();
if (fgets(name, SIZE, stdin) == NULL) {
printf("Entrada inválida\n");
return 1;
}
printf("Edad: %d\n", age);
printf("Nombre: %s", name);
return 0;
}// Un int se copia al llamar. Un arreglo no se copia: se pasa su dirección.
#include <stdio.h>
#define N 3
void add_one(int x) {
x = x + 1;
}
void add_bonus(int a[], int n, int bonus) {
for (int i = 0; i < n; i++) {
a[i] = a[i] + bonus;
}
}
int main(void) {
int grade = 14;
add_one(grade);
printf("grade: %d\n", grade);
int grades[N] = {14, 9, 17};
add_bonus(grades, N, 2);
printf("grades: %d %d %d\n", grades[0], grades[1], grades[2]);
return 0;
}// Parte de las pruebas del laboratorio: test_lab.py llama con esto a sus funciones.
// No hace falta leerlo. Usa argumentos de la línea de comandos, que no vemos en el curso.
#include <stdio.h>
int my_strlen(const char s[]);
int my_strcmp(const char a[], const char b[]);
void to_upper(char s[]);
int is_palindrome(const char s[]);
int is_palindrome_text(const char s[]);
// Copia el texto a un arreglo propio, para que la función reciba memoria de la pila.
static void copy(char dest[], const char src[], int size) {
int i = 0;
while (src[i] != '\0' && i < size - 1) {
dest[i] = src[i];
i++;
}
dest[i] = '\0';
}
int main(int argc, char *argv[]) {
if (argc < 3) {
return 2;
}
char op = argv[1][0];
char first[64];
char second[64];
copy(first, argv[2], 64);
copy(second, argc > 3 ? argv[3] : "", 64);
if (op == 'l') {
printf("%d\n", my_strlen(first));
} else if (op == 'c') {
printf("%d\n", my_strcmp(first, second));
} else if (op == 'u') {
to_upper(first);
printf("%s\n", first);
} else if (op == 'p') {
printf("%d\n", is_palindrome(first));
} else if (op == 't') {
printf("%d\n", is_palindrome_text(first));
} else {
return 2;
}
return 0;
}// const en un parámetro: la función promete que no cambia el arreglo.
#include <stdio.h>
#define N 5
// Requiere n >= 1.
int max_value(const int a[], int n) {
int max = a[0];
for (int i = 1; i < n; i++) {
if (a[i] > max) {
max = a[i];
}
}
return max;
}
int main(void) {
int grades[N] = {14, 9, 17, 11, 20};
printf("Nota mayor: %d\n", max_value(grades, N));
return 0;
}// Copia una cadena sin pasarse: quien escribe en un arreglo recibe su tamaño.
#include <stdio.h>
#define SIZE 5
// Requiere size >= 1. Copia como máximo size - 1 bytes y siempre termina en '\0'.
void copy_text(char dest[], const char src[], int size) {
int i = 0;
while (src[i] != '\0' && i < size - 1) {
dest[i] = src[i];
i++;
}
dest[i] = '\0';
}
int main(void) {
char word[SIZE];
copy_text(word, "sol", SIZE);
printf("%s\n", word);
copy_text(word, "luna", SIZE);
printf("%s\n", word);
copy_text(word, "estrella", SIZE);
printf("%s\n", word);
return 0;
}// Recorre una cadena y cuenta las minúsculas. Funciona solo para ASCII.
#include <stdio.h>
int count_lowercase(const char s[]) {
int count = 0;
for (int i = 0; s[i] != '\0'; i++) {
if (s[i] >= 'a' && s[i] <= 'z') {
count++;
}
}
return count;
}
int main(void) {
printf("Hola Mundo: %d\n", count_lowercase("Hola Mundo"));
printf("LIMA 2026: %d\n", count_lowercase("LIMA 2026"));
printf("ano: %d\n", count_lowercase("ano"));
printf("año: %d\n", count_lowercase("año"));
return 0;
}// %s se detiene en el primer '\0', aunque el arreglo siga.
#include <stdio.h>
int main(void) {
char s[] = "hola";
printf("%s\n", s);
s[0] = 'H';
printf("%s\n", s);
s[2] = '\0';
printf("%s\n", s);
printf("sizeof(s) sigue siendo %zu\n", sizeof(s));
return 0;
}// La longitud de una cadena: los bytes que hay antes del '\0'.
#include <assert.h>
#include <stdio.h>
int my_strlen(const char s[]) {
int i = 0;
while (s[i] != '\0') {
i++;
}
return i;
}
int main(void) {
assert(my_strlen("") == 0);
assert(my_strlen("a") == 1);
assert(my_strlen("hola") == 4);
char t[8] = "hola";
printf("my_strlen(t) = %d\n", my_strlen(t));
printf("sizeof(t) = %zu\n", sizeof(t));
printf("my_strlen(\"ano\") = %d\n", my_strlen("ano"));
printf("my_strlen(\"año\") = %d\n", my_strlen("año"));
return 0;
}// Muestra qué deja fgets en el buffer en cada lectura.
#include <stdio.h>
#define SIZE 8
// Escribe la cadena con \n visible, para ver si el salto de línea está.
void print_visible(const char s[]) {
for (int i = 0; s[i] != '\0'; i++) {
if (s[i] == '\n') {
printf("\\n");
} else {
printf("%c", s[i]);
}
}
}
int main(void) {
char line[SIZE];
while (fgets(line, SIZE, stdin) != NULL) {
printf("leído: \"");
print_visible(line);
printf("\"\n");
}
printf("fgets devolvió NULL\n");
return 0;
}// Lee dos líneas con fgets. Quita el salto de línea y descarta lo que no cupo.
#include <stdio.h>
#define SIZE 20
// Si la cadena tiene '\n', lo quita y devuelve 1. Si no, devuelve 0.
int remove_newline(char s[]) {
for (int i = 0; s[i] != '\0'; i++) {
if (s[i] == '\n') {
s[i] = '\0';
return 1;
}
}
return 0;
}
// Lee y descarta lo que queda de la línea actual.
void discard_rest(void) {
int c = getchar();
while (c != '\n' && c != EOF) {
c = getchar();
}
}
// Lee una línea en s. Devuelve 0 si la entrada ya terminó y 1 si leyó algo.
int read_text(char s[], int size) {
if (fgets(s, size, stdin) == NULL) {
return 0;
}
if (!remove_newline(s)) {
discard_rest();
}
return 1;
}
int main(void) {
char name[SIZE];
char city[SIZE];
if (!read_text(name, SIZE) || !read_text(city, SIZE)) {
printf("Entrada inválida\n");
return 1;
}
printf("Nombre: %s\n", name);
printf("Ciudad: %s\n", city);
return 0;
}// %19s lee una palabra de 19 bytes como máximo: deja lugar para el '\0'.
#include <stdio.h>
int main(void) {
char word[20];
if (scanf("%19s", word) != 1) {
printf("Entrada inválida\n");
return 1;
}
printf("Primera lectura: %s\n", word);
if (scanf("%19s", word) != 1) {
printf("No hay más palabras\n");
return 0;
}
printf("Segunda lectura: %s\n", word);
return 0;
}// La función recibe la dirección donde empieza el arreglo de main.
#include <stdio.h>
void show_address(int a[]) {
printf("en la función, a: %p\n", (void *) a);
}
int main(void) {
int grades[3] = {14, 9, 17};
printf("en main, grades: %p\n", (void *) grades);
show_address(grades);
return 0;
}// Muestra los bytes de dos cadenas iguales guardadas en arreglos de distinto tamaño.
#include <stdio.h>
int main(void) {
char s[] = "hola";
char t[8] = "hola";
printf("sizeof(s) = %zu\n", sizeof(s));
for (int i = 0; i < (int) sizeof(s); i++) {
printf("s[%d] = %d\n", i, s[i]);
}
printf("sizeof(t) = %zu\n", sizeof(t));
for (int i = 0; i < (int) sizeof(t); i++) {
printf(" %d", t[i]);
}
printf("\n");
printf("'\\0' vale %d y '0' vale %d\n", '\0', '0');
return 0;
}// Pasa un arreglo a una función: la longitud va en otro parámetro.
#include <stdio.h>
#define N 5
int sum(int a[], int n) {
int total = 0;
for (int i = 0; i < n; i++) {
total = total + a[i];
}
return total;
}
int main(void) {
int grades[N] = {14, 9, 17, 11, 20};
int total = sum(grades, N);
printf("Suma de las 5 notas: %d\n", total);
printf("Promedio: %.2f\n", (double) total / N);
printf("Suma de las 3 primeras: %d\n", sum(grades, 3));
return 0;
}// Mantener un vector: un arreglo con capacidad fija y una longitud que cambia.
#include <stdio.h>
#define CAPACITY 5
void print_vector(const int v[], int n) {
printf("n = %d:", n);
for (int i = 0; i < n; i++) {
printf(" %d", v[i]);
}
printf("\n");
}
// Inserta value en la posición pos. Devuelve la longitud nueva.
// Si no se puede, no cambia nada y devuelve la misma longitud.
int insert_at(int v[], int n, int capacity, int pos, int value) {
if (n >= capacity || pos < 0 || pos > n) {
return n;
}
for (int i = n; i > pos; i--) {
v[i] = v[i - 1];
}
v[pos] = value;
return n + 1;
}
// Elimina el elemento de la posición pos. Devuelve la longitud nueva.
// Si no se puede, no cambia nada y devuelve la misma longitud.
int remove_at(int v[], int n, int pos) {
if (pos < 0 || pos >= n) {
return n;
}
for (int i = pos; i < n - 1; i++) {
v[i] = v[i + 1];
}
return n - 1;
}
int main(void) {
int v[CAPACITY] = {10, 20, 30};
int n = 3;
print_vector(v, n);
n = insert_at(v, n, CAPACITY, 1, 99);
print_vector(v, n);
n = insert_at(v, n, CAPACITY, 4, 40);
print_vector(v, n);
n = insert_at(v, n, CAPACITY, 0, 5); // ya está lleno: no cambia
print_vector(v, n);
n = remove_at(v, n, 0);
print_vector(v, n);
n = remove_at(v, n, 3);
print_vector(v, n);
n = remove_at(v, n, 7); // no existe: no cambia
print_vector(v, n);
return 0;
}Errores
Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.
// A propósito: una función con const en el parámetro intenta cambiar el arreglo.
#include <stdio.h>
void reset(const int a[], int n) {
for (int i = 0; i < n; i++) {
a[i] = 0;
}
}
int main(void) {
int grades[3] = {14, 9, 17};
reset(grades, 3);
printf("%d\n", grades[0]);
return 0;
}// A propósito: abre el espacio moviendo desde el inicio y pisa los valores.
#include <stdio.h>
int main(void) {
int v[5] = {10, 20, 30};
int n = 3;
int pos = 1;
for (int i = pos; i < n; i++) {
v[i + 1] = v[i];
}
v[pos] = 99;
n = n + 1;
for (int i = 0; i < n; i++) {
printf(" %d", v[i]);
}
printf("\n");
return 0;
}// A propósito: intenta cambiar un literal de cadena. Es comportamiento indefinido.
#include <stdio.h>
void set_first(char s[], char c) {
s[0] = c;
}
int main(void) {
char copy[] = "hola";
set_first(copy, 'H');
printf("%s\n", copy);
set_first("hola", 'H');
printf("no llegamos aquí, o no deberíamos\n");
return 0;
}// A propósito: word tiene 4 bytes y ninguno es '\0'. No es una cadena.
#include <stdio.h>
int main(void) {
char word[4] = "hola";
printf("%s\n", word);
return 0;
}// A propósito: name es pequeño y al lado puede haber otra variable importante.
#include <stdio.h>
int main(void) {
int is_admin = 0;
char name[8];
if (scanf("%s", name) != 1) {
printf("Entrada inválida\n");
return 1;
}
printf("Hola, %s\n", name);
if (is_admin != 0) {
printf("Acceso de administrador (is_admin = %d)\n", is_admin);
} else {
printf("Acceso normal\n");
}
return 0;
}// A propósito: %s sin límite puede escribir fuera de name.
#include <stdio.h>
int main(void) {
char name[8];
if (scanf("%s", name) != 1) {
printf("Entrada inválida\n");
return 1;
}
printf("Hola, %s\n", name);
return 0;
}// A propósito: scanf deja el salto de línea y fgets lo lee como una línea vacía.
#include <stdio.h>
#define SIZE 20
int main(void) {
int age;
char name[SIZE];
if (scanf("%d", &age) != 1) {
printf("Entrada inválida\n");
return 1;
}
if (fgets(name, SIZE, stdin) == NULL) {
printf("Entrada inválida\n");
return 1;
}
printf("Edad: %d\n", age);
printf("Nombre: %s", name);
return 0;
}// A propósito: sizeof de un parámetro arreglo no da el tamaño del arreglo.
#include <stdio.h>
void print_size(int a[]) {
printf("en la función: %zu bytes\n", sizeof(a));
}
void print_size_five(int a[5]) {
printf("con [5] en el parámetro: %zu bytes\n", sizeof(a));
}
int main(void) {
int grades[5] = {14, 9, 17, 11, 20};
printf("en main: %zu bytes\n", sizeof(grades));
print_size(grades);
print_size_five(grades);
return 0;
}// A propósito: el texto no cabe en el arreglo.
#include <stdio.h>
int main(void) {
char word[3] = "hola";
printf("%c\n", word[0]);
return 0;
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
"""Comprueba lo que los apuntes afirman sobre los programas de la semana 10."""
import pathlib
import re
import subprocess
import tempfile
import unittest
HERE = pathlib.Path(__file__).parent
BUILD = HERE / "build"
FLAGS = ["-Wall", "-Wextra", "-std=c17"]
SANITIZE = ["-g", "-fsanitize=address,undefined"]
def execute(command, stdin=""):
done = subprocess.run(
command, input=stdin, encoding="utf-8", errors="replace",
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10,
)
return done.stdout, done.returncode
def run(program, stdin=""):
"""Ejecuta un programa de build/; devuelve su salida (con errores) y su estado."""
return execute([str(BUILD / program)], stdin)
def build_source(source, *extra):
"""Compila un archivo; devuelve los mensajes, el estado y el ejecutable."""
out = pathlib.Path(tempfile.mkdtemp()) / pathlib.Path(source).stem
done = subprocess.run(
["gcc", *FLAGS, *extra, source, "-o", str(out)], cwd=HERE, encoding="utf-8",
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
return done.stdout, done.returncode, out
class CleanCompile(unittest.TestCase):
def test_examples_have_no_warnings(self):
for source in sorted(HERE.glob("*.c")):
with self.subTest(source=source.name):
done = subprocess.run(
["gcc", *FLAGS, "-Werror", "-fsyntax-only", str(source)],
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
self.assertEqual(done.returncode, 0, done.stdout)
def test_string_h_is_not_used_yet(self):
for source in list(HERE.glob("*.c")) + list((HERE / "errors").glob("*.c")):
with self.subTest(source=source.name):
self.assertNotIn("string.h", source.read_text(encoding="utf-8"))
class ArraysInFunctions(unittest.TestCase):
def test_sum_uses_the_length_it_receives(self):
self.assertEqual(run("sum_array"), (
"Suma de las 5 notas: 71\nPromedio: 14.20\nSuma de las 3 primeras: 40\n", 0))
def test_function_changes_array_but_not_int(self):
self.assertEqual(run("bonus"), ("grade: 14\ngrades: 16 11 19\n", 0))
def test_function_sees_the_same_address(self):
output, _ = run("same_address")
addresses = re.findall(r"0x[0-9a-f]+", output)
self.assertEqual(len(addresses), 2)
self.assertEqual(addresses[0], addresses[1])
def test_sizeof_of_parameter(self):
messages, status, binary = build_source("errors/sizeof_param.c")
self.assertEqual(status, 0)
self.assertEqual(messages.count("-Wsizeof-array-argument"), 2)
output, _ = execute([str(binary)])
sizes = re.findall(r"(\d+) bytes", output)
self.assertEqual(sizes[0], "20") # 5 int de 4 bytes en este entorno
self.assertEqual(sizes[1], sizes[2]) # el 5 del parámetro no cambia nada
self.assertNotEqual(sizes[1], "20")
def test_const_is_enforced(self):
messages, status, _ = build_source("errors/const_write.c")
self.assertNotEqual(status, 0)
self.assertIn("read-only location", messages)
def test_const_accepts_a_normal_array(self):
self.assertEqual(run("const_param"), ("Nota mayor: 20\n", 0))
class Vector(unittest.TestCase):
def test_insert_and_remove_with_their_limits(self):
expected = ["n = 3: 10 20 30", "n = 4: 10 99 20 30", "n = 5: 10 99 20 30 40",
"n = 5: 10 99 20 30 40", "n = 4: 99 20 30 40", "n = 3: 99 20 30",
"n = 3: 99 20 30"]
output, status = run("vector")
self.assertEqual((output.splitlines(), status), (expected, 0))
def test_moving_from_the_start_loses_a_value(self):
_, _, binary = build_source("errors/insert_wrong.c")
self.assertEqual(execute([str(binary)])[0], " 10 99 20 20\n")
class Strings(unittest.TestCase):
def test_array_sizes_and_bytes(self):
output, _ = run("string_bytes")
self.assertIn("sizeof(s) = 5\n", output)
self.assertIn("s[4] = 0\n", output)
self.assertIn("sizeof(t) = 8\n 104 111 108 97 0 0 0 0\n", output)
self.assertIn("'\\0' vale 0 y '0' vale 48\n", output)
def test_terminator_cuts_the_text_but_not_the_array(self):
self.assertEqual(run("cut_string")[0], "hola\nHola\nHo\nsizeof(s) sigue siendo 5\n")
def test_length_counts_bytes(self):
output, status = run("my_strlen") # sus assert también corren aquí
self.assertEqual(status, 0)
self.assertIn('my_strlen("ano") = 3\n', output)
self.assertIn('my_strlen("año") = 4\n', output)
self.assertIn("my_strlen(t) = 4\nsizeof(t) = 8\n", output)
def test_lowercase_ignores_utf8_with_either_char_sign(self):
expected = "Hola Mundo: 7\nLIMA 2026: 0\nano: 3\naño: 2\n"
self.assertEqual(run("count_lowercase")[0], expected)
for flag in ["-fsigned-char", "-funsigned-char"]:
with self.subTest(flag=flag):
_, status, binary = build_source("count_lowercase.c", flag)
self.assertEqual(status, 0)
self.assertEqual(execute([str(binary)])[0], expected)
def test_too_long_initializer_warns_but_one_byte_short_does_not(self):
messages, _, _ = build_source("errors/too_long.c")
self.assertIn("is too long", messages)
messages, status, _ = build_source("errors/no_terminator.c")
self.assertEqual((messages, status), ("", 0))
def test_no_terminator_is_detected_when_running(self):
_, _, binary = build_source("errors/no_terminator.c", *SANITIZE)
output, status = execute([str(binary)])
self.assertNotEqual(status, 0)
self.assertIn("stack-buffer-overflow", output)
def test_writing_a_literal_compiles_without_warnings(self):
# Al correr es comportamiento indefinido: de eso no se prueba nada.
messages, status, _ = build_source("errors/literal_write.c")
self.assertEqual((messages, status), ("", 0))
class ReadingText(unittest.TestCase):
def test_width_limits_scanf_and_stops_at_space(self):
self.assertEqual(run("read_word", "Ana María\n")[0],
"Primera lectura: Ana\nSegunda lectura: María\n")
self.assertEqual(run("read_word", "abcdefghijklmnopqrstuvwxyz\n")[0],
"Primera lectura: abcdefghijklmnopqrs\nSegunda lectura: tuvwxyz\n")
self.assertEqual(run("read_word", ""), ("Entrada inválida\n", 1))
def test_fgets_four_cases(self):
cases = {
"Ana\n": 'leído: "Ana\\n"\nfgets devolvió NULL\n',
"Ana": 'leído: "Ana"\nfgets devolvió NULL\n',
"Alejandra\n": 'leído: "Alejand"\nleído: "ra\\n"\nfgets devolvió NULL\n',
"": "fgets devolvió NULL\n",
}
for text, expected in cases.items():
with self.subTest(text=text):
self.assertEqual(run("read_line", text), (expected, 0))
def test_exact_fit_leaves_the_newline_for_later(self):
self.assertEqual(run("read_line", "Rosaura\n")[0],
'leído: "Rosaura"\nleído: "\\n"\nfgets devolvió NULL\n')
def test_read_name(self):
self.assertEqual(run("read_name", "Ana María\nLima\n"),
("Nombre: Ana María\nCiudad: Lima\n", 0))
self.assertEqual(run("read_name", "Maria de los Angeles Quispe\nLima\n"),
("Nombre: Maria de los Angele\nCiudad: Lima\n", 0))
# 19 bytes justos: el salto de línea que queda no debe llegar a la ciudad.
self.assertEqual(run("read_name", "abcdefghijklmnopqrs\nLima\n"),
("Nombre: abcdefghijklmnopqrs\nCiudad: Lima\n", 0))
self.assertEqual(run("read_name", "Ana\n"), ("Entrada inválida\n", 1))
self.assertEqual(run("read_name", ""), ("Entrada inválida\n", 1))
def test_scanf_leaves_the_newline(self):
_, _, binary = build_source("errors/scanf_then_fgets.c")
self.assertEqual(execute([str(binary)], "18\nAna\n")[0], "Edad: 18\nNombre: \n")
self.assertEqual(run("age_name", "18\nAna\n")[0], "Edad: 18\nNombre: Ana\n")
class BufferOverflow(unittest.TestCase):
def test_short_name_works_and_long_name_is_reported(self):
messages, status, binary = build_source("errors/overflow_scanf.c", *SANITIZE)
self.assertEqual((messages, status), ("", 0))
self.assertEqual(execute([str(binary)], "Ana\n"), ("Hola, Ana\n", 0))
output, status = execute([str(binary)], "Alejandra\n")
self.assertNotEqual(status, 0)
for fragment in ["stack-buffer-overflow", "WRITE of size 10",
"overflow_scanf.c:7", "'name' (line 5)"]:
self.assertIn(fragment, output)
def test_overflow_demos_compile_without_warnings(self):
for source in ["errors/overflow_scanf.c", "errors/overflow_neighbor.c"]:
with self.subTest(source=source):
messages, status, _ = build_source(source)
self.assertEqual((messages, status), ("", 0))
def test_neighbor_demo_is_caught_by_the_sanitizer(self):
# Sin sanitizer el resultado depende de la máquina: no se compara.
_, _, binary = build_source("errors/overflow_neighbor.c", *SANITIZE)
self.assertIn("Acceso normal", execute([str(binary)], "Ana\n")[0])
self.assertIn("stack-buffer-overflow", execute([str(binary)], "Alejandra_Quispe\n")[0])
def test_copy_text_stays_inside(self):
self.assertEqual(run("copy_text"), ("sol\nluna\nestr\n", 0))
if __name__ == "__main__":
unittest.main()"""Pruebas del laboratorio. LAB_DIR dice dónde está el código: lab o solution."""
import os
import pathlib
import subprocess
import unittest
HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
def run(program, stdin=""):
done = subprocess.run(
[str(HERE / "build" / LAB_DIR / program)], input=stdin, encoding="utf-8", errors="replace",
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10,
)
return done.stdout, done.returncode
def call(*args):
"""Llama a una función de my_strings.c y devuelve lo que imprime el programa de prueba."""
done = subprocess.run(
[str(HERE / "build" / LAB_DIR / "check_strings")] + [a.encode("utf-8") for a in args],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10,
)
return done.stdout.decode("utf-8", "replace").strip()
@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe {LAB_DIR}/")
class LabTests(unittest.TestCase):
def test_no_warnings(self):
for name in ["my_strings", "greet"]:
with self.subTest(name=name):
done = subprocess.run(
["gcc", "-Wall", "-Wextra", "-std=c17", "-Werror", "-fsyntax-only",
str(HERE / LAB_DIR / f"{name}.c")],
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
self.assertEqual(done.returncode, 0, done.stdout)
def test_does_not_use_string_h(self):
text = (HERE / LAB_DIR / "my_strings.c").read_text(encoding="utf-8")
self.assertNotRegex(text, r"#\s*include\s*<string\.h>")
def test_your_own_asserts_pass(self):
output, code = run("my_strings")
self.assertEqual((output, code), ("Todas las pruebas pasaron\n", 0))
def test_my_strlen(self):
for text, expected in {"": "0", "a": "1", "hola": "4", "dos palabras": "12",
"año": "4", "ñandú": "7"}.items():
with self.subTest(text=text):
self.assertEqual(call("l", text), expected)
def test_my_strcmp_equal(self):
for text in ["", "a", "ana", "año"]:
with self.subTest(text=text):
self.assertEqual(call("c", text, text), "0")
def test_my_strcmp_order(self):
# Solo importa el signo. Cada par (a, b) cumple que a va antes que b.
pairs = [("ana", "ano"), ("Z", "a"), ("an", "ana"), ("", "a"), ("a", "b"), ("z", "ñ")]
for a, b in pairs:
with self.subTest(a=a, b=b):
self.assertLess(int(call("c", a, b)), 0)
self.assertGreater(int(call("c", b, a)), 0)
def test_to_upper(self):
cases = {"hola": "HOLA", "": "", "Ya ES": "YA ES", "lima 2026!": "LIMA 2026!",
"az{`": "AZ{`", "año": "AñO"}
for text, expected in cases.items():
with self.subTest(text=text):
self.assertEqual(call("u", text), expected)
def test_is_palindrome(self):
cases = {"": "1", "a": "1", "aa": "1", "ab": "0", "abba": "1", "reconocer": "1",
"abca": "0", "Ana": "0", "ana ana": "1", "a a": "1", "ana a": "0",
"aña": "0"}
for text, expected in cases.items():
with self.subTest(text=text):
self.assertEqual(call("p", text), expected)
def test_is_palindrome_text(self):
cases = {"": "1", " ": "1", "a": "1", " a ": "1", "Ana": "1", "ANA": "1", "ab": "0",
"Anita lava la tina": "1", " a b B A": "1", "Hola": "0", "a b c": "0"}
for text, expected in cases.items():
with self.subTest(text=text):
self.assertEqual(call("t", text), expected)
def test_greet(self):
cases = {"Ana\n": "Hola, Ana\n", "Ana": "Hola, Ana\n", "Ana Li\n": "Hola, Ana Li\n",
"Rosaura\n": "Hola, Rosaura\n", "Alejandra\n": "Hola, Alejand\n"}
for text, expected in cases.items():
with self.subTest(text=text):
self.assertEqual(run("greet", text), (expected, 0))
def test_greet_no_name(self):
for text in ["", "\n"]:
with self.subTest(text=text):
self.assertEqual(run("greet", text), ("Entrada inválida\n", 1))
if __name__ == "__main__":
unittest.main()"""Vuelve a correr los comandos que aparecen en los apuntes y en las diapositivas
y comprueba que el código citado existe de verdad en esta carpeta."""
import pathlib
import re
import subprocess
import unittest
HERE = pathlib.Path(__file__).parent
ROOT = HERE.parent.parent
NOTES = ROOT / "notes" / "week-10.md"
SLIDES = ROOT / "slides" / "week-10.tex"
# Antes de un bloque con direcciones o con comportamiento indefinido. De él solo se
# exigen estas cadenas estables, si el bloque las muestra.
MACHINE_DEPENDENT = "<!-- depende de la corrida -->"
STABLE = ["AddressSanitizer: stack-buffer-overflow"]
# En esta salida, solo las direcciones cambian.
ADDRESSES = re.compile(r"0x[0-9a-f]+")
def normalize(text):
# Según el idioma del entorno, gcc usa comillas tipográficas o rectas.
return ADDRESSES.sub("0x", text.replace("‘", "'").replace("’", "'"))
def run_shell(command):
done = subprocess.run(
["bash", "-c", command], cwd=HERE, encoding="utf-8", errors="replace",
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
)
return normalize(done.stdout)
def split_commands(lines):
"""Parte un bloque de terminal en pares (comando, líneas de salida)."""
pairs = []
for line in lines:
if line.startswith("$ "):
pairs.append((line[2:], []))
elif pairs:
pairs[-1][1].append(normalize(line.rstrip()))
return pairs
def note_blocks(language):
"""Devuelve (líneas, depende_de_la_corrida) de cada bloque de los apuntes."""
blocks, current, flagged, previous = [], None, False, ""
for raw in NOTES.read_text(encoding="utf-8").splitlines():
line = re.sub(r"^> ?", "", raw)
if current is None:
if line.strip() == "```" + language:
current, flagged = [], previous.strip() == MACHINE_DEPENDENT
elif line.strip():
previous = line
elif line.strip() == "```":
blocks.append((current, flagged))
current = None
else:
current.append(line)
return blocks
def slide_blocks(environments):
text = SLIDES.read_text(encoding="utf-8")
pattern = r"\\begin\{(?:%s)\}[^\n]*\n(.*?)\\end\{(?:%s)\}" % (environments, environments)
return [match.group(1).splitlines() for match in re.finditer(pattern, text, re.S)]
def known_code_lines():
lines = set()
for source in list(HERE.glob("*.c")) + list((HERE / "errors").glob("*.c")):
lines.update(line.strip() for line in source.read_text(encoding="utf-8").splitlines())
return lines
class NotesAreTrue(unittest.TestCase):
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_terminal_blocks_in_notes(self):
checked = 0
for lines, flagged in note_blocks("text"):
if not lines or not lines[0].startswith("$ "):
continue
for command, expected in split_commands(lines):
actual = run_shell(command).rstrip("\n")
checked += 1
with self.subTest(command=command):
if flagged:
for token in STABLE:
if any(token in line for line in expected):
self.assertIn(token, actual)
else:
lines = [line.rstrip() for line in actual.split("\n")]
self.assertEqual("\n".join(lines), "\n".join(expected))
self.assertGreater(checked, 20)
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_code_blocks_in_notes_exist(self):
known = known_code_lines()
for lines, _ in note_blocks("c"):
for line in lines:
if line.strip() and line.strip() != "...":
with self.subTest(line=line):
self.assertIn(line.strip(), known)
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_notes_show_the_report_and_flag_undefined_behavior(self):
text = NOTES.read_text(encoding="utf-8")
self.assertIn("WRITE of size 10", text)
self.assertIn("en esta corrida", " ".join(text.split()))
class SlidesAreTrue(unittest.TestCase):
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
def test_terminal_blocks_in_slides(self):
for block in slide_blocks("terminal|salidalarga"):
for command, expected in split_commands(block):
actual = run_shell(command).splitlines()
position = 0
# Una diapositiva puede omitir líneas, pero no cambiarlas ni reordenarlas.
for line in expected:
line = line.strip()
if not line:
continue
with self.subTest(command=command, line=line):
found = [i for i in range(position, len(actual)) if actual[i].strip() == line]
self.assertTrue(found, f"no aparece en la salida real: {line}")
if found:
position = found[0] + 1
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
def test_code_blocks_in_slides_exist(self):
known = known_code_lines()
for block in slide_blocks("codigo|codigochico|codigomini"):
for line in block:
if line.strip() and line.strip() != "...":
with self.subTest(line=line):
self.assertIn(line.strip(), known)
if __name__ == "__main__":
unittest.main()Laboratorio
Archivos de partida del laboratorio de la semana.
// Laboratorio, parte 3: este programa tiene un buffer overflow a propósito.
//
// Primero provóquenlo: escriban un nombre de más de 7 letras y lean el informe de
// -fsanitize=address. Después corríjanlo con fgets, para que el programa:
//
// - lea una línea completa, también con espacios, en name;
// - quite el salto de línea, si está;
// - escriba "Hola, " y el nombre, y termine con return 0;
// - guarde solo los primeros 7 bytes si el nombre no cabe, y escriba eso;
// - escriba "Entrada inválida" y termine con return 1 si no hay ninguna línea o si
// la línea está vacía.
//
// No cambien SIZE. Pruébenlo con make test-lab.
#include <stdio.h>
#define SIZE 8
int main(void) {
char name[SIZE];
if (scanf("%s", name) != 1) {
printf("Entrada inválida\n");
return 1;
}
printf("Hola, %s\n", name);
return 0;
}// Laboratorio, partes 1 y 2: funciones de cadenas, sin string.h.
//
// Escriban cada función según su comentario. Borren la línea (void) de cada una:
// solo está para que el archivo compile sin advertencias mientras tanto.
// Las cadenas pueden traer bytes de UTF-8, como los dos de la ñ.
//
// En main hay una prueba de cada función. Agreguen más con assert: casos normales
// y bordes, como la cadena vacía.
//
// Pruébenlo con make test-lab.
#include <assert.h>
#include <stdio.h>
// Devuelve cuántos bytes hay antes del '\0'.
int my_strlen(const char s[]) {
(void) s;
return 0;
}
// Compara a y b byte por byte, hasta la primera diferencia o hasta el final.
// Devuelve 0 si son iguales. Devuelve un número negativo si a va antes que b, y uno
// positivo si a va después. Una cadena que es el inicio de otra va antes.
// Comparen los bytes como unsigned char, para que el resultado no dependa de la máquina.
int my_strcmp(const char a[], const char b[]) {
(void) a;
(void) b;
return 0;
}
// Convierte a mayúscula las letras de la 'a' a la 'z', en el mismo arreglo.
// Todos los demás bytes quedan igual.
void to_upper(char s[]) {
(void) s;
}
// Devuelve 1 si s se lee igual byte por byte de izquierda a derecha y de derecha a
// izquierda, y 0 si no. Sin ninguna excepción: "Ana" no es palíndromo, porque
// 'A' y 'a' son bytes distintos. La cadena vacía sí lo es.
int is_palindrome(const char s[]) {
(void) s;
return 0;
}
// Igual que is_palindrome, pero ignora los espacios y no distingue entre mayúsculas
// y minúsculas de la 'a' a la 'z'. "Anita lava la tina" sí es palíndromo.
// s no se puede cambiar. Una cadena sin nada más que espacios sí es palíndromo.
int is_palindrome_text(const char s[]) {
(void) s;
return 0;
}
int main(void) {
assert(my_strlen("hola") == 4);
assert(my_strcmp("ana", "ana") == 0);
char word[] = "hola";
to_upper(word);
assert(word[0] == 'H');
assert(is_palindrome("reconocer") == 1);
assert(is_palindrome_text("Anita lava la tina") == 1);
printf("Todas las pruebas pasaron\n");
return 0;
}Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 10: arreglos en funciones y cadenas. Se usa dentro del entorno del curso.
#
# make compila los programas de la clase en build/
# make test corre las pruebas de los ejemplos y de los apuntes
# make test-lab prueba su laboratorio (lab/*.c); falla hasta que lo terminen
# make clean borra lo generado
#
# Los programas de errors/ tienen errores a propósito. No se compilan con make:
# los compilan las pruebas, y ustedes, con los comandos de los apuntes.
#
# check_strings.c es parte de las pruebas: llama a las funciones de my_strings.c.
# Para enlazarlos, el main de my_strings.c se compila con otro nombre. Se compila con
# -fsigned-char para que make test-lab dé lo mismo en todas las máquinas.
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g -fsanitize=address,undefined
BUILD = build
PROGRAMS = sum_array bonus same_address const_param vector string_bytes cut_string \
my_strlen count_lowercase read_word read_line read_name age_name copy_text
.PHONY: all lab test test-lab clean
all: $(PROGRAMS:%=$(BUILD)/%)
$(BUILD)/%: %.c | $(BUILD)
$(CC) $(CFLAGS) $< -o $@
$(BUILD) $(BUILD)/lab $(BUILD)/solution:
mkdir -p $@
$(BUILD)/lab/% : lab/%.c | $(BUILD)/lab
$(CC) $(CFLAGS) $< -o $@
$(BUILD)/solution/% : solution/%.c | $(BUILD)/solution
$(CC) $(CFLAGS) $< -o $@
$(BUILD)/lab/check_strings: check_strings.c lab/my_strings.c | $(BUILD)/lab
$(CC) $(CFLAGS) -fsigned-char -Dmain=student_main -c lab/my_strings.c -o $(BUILD)/lab/my_strings.o
$(CC) $(CFLAGS) -fsigned-char check_strings.c $(BUILD)/lab/my_strings.o -o $@
$(BUILD)/solution/check_strings: check_strings.c solution/my_strings.c | $(BUILD)/solution
$(CC) $(CFLAGS) -fsigned-char -Dmain=student_main -c solution/my_strings.c -o $(BUILD)/solution/my_strings.o
$(CC) $(CFLAGS) -fsigned-char check_strings.c $(BUILD)/solution/my_strings.o -o $@
LAB_FILES = my_strings greet check_strings
lab: $(LAB_FILES:%=$(BUILD)/lab/%)
# Si existe solution/, también se comprueba la solución del laboratorio.
test: all
@if [ -d solution ]; then $(MAKE) --no-print-directory \
$(LAB_FILES:%=$(BUILD)/solution/%); fi
python3 -m unittest discover -p 'test_*.py'
test-lab: all lab
LAB_DIR=lab python3 -m unittest -v test_lab
clean:
rm -rf $(BUILD) __pycache__