Zadania

  1. chmod.c: przygotuj uproszczoną implementację programu chmod, która może zostać wywołana przez użytkownika w następującej formie ./chmod <wyrażenie> <plik1> <plik2> …​ <plikn>, gdzie:

    1. plik1…​plikn to pliki, które mają otrzymać wybrane uprawnienia

    2. wyrażenie to wyrażenie, które określa jak należy zmienic uprawnienia, odpowiadające następującemu wyrażeniu regularnemu [ugoa][=-+][rwx]+, gdzie:

      1. u oznacza uprawnienia przypisane użytkownikowi, g grupie, o pozostałym

      2. + oznacza dodanie uprawnień do istniejącego zbioru, - usunięcie uprawnień z istniejącego zbioru, = nadpisanie uprawnień

      3. r oznacza prawo do odczytu, w do zapisu, x do wykonania

    3. implementacja wykorzystuje zdefiniowane przez osobę studencką flagi bitowe przy pomocy typu wyliczeniowego!

    4. po zastosowaniu uprawnień wypisywane jest podsumowanie aktualnego stanu uprawnień

Przykładowy opis pracy z zadaniem

Jak podejść do zadania takiego typu? Należy zauważyć, że zadanie składa się z kilku części, które optymalnie jest wykonać w następującej kolejności:

  1. Wczytanie argumentów i poprawne ich parsowanie

  2. Sprawdzenie uprawnień danego pliku

  3. Implementacji wypisywania podsumowania stanu uprawnień

  4. Zapisanie uprawnień

Pozwala to na rozbicie programu na kilka funkcji (nie muszą być to funkcje, mogą być to podsekcje funkcji main):

  • void parse(char const* decl, enum permissions *perm, enum action *action); - parsujący decl i na jej podstawie ustawiający odpowiednio wartości perm i action. Przykładowe wywołania:

    1. parse("a+r", &p, &a); ustawia p = USER_READ | GROUP_READ | OTHER_READ, a = ADD

    2. parse("o-rwx", &p, &a); ustawia p = OTHER_READ | OTHER_WRITE | OTHER_EXEC, a = DEL

    3. parse("a=r", &p, &a); ustawia p = USER_READ | GROUP_READ | OTHER_READ, a = SET

  • enum permissions apply(enum permissions old, enum permissions new, enum action how); - zwraca nowy zbiór parametrów, będący wynikiem zaaplikowania new na old według metody how:

    1. apply(USER_READ | USER_WRITE, USER_READ | GROUP_READ, OTHER_READ, ADD) zwraca USER_READ | USER_WRITE | GROUP_READ | OTHER_READ

    2. apply(USER_WRITE | USER_READ, USER_WRITE, DEL) zwraca USER_READ

    3. apply(USER_WRITE | USER_READ, USER_READ | USER_EXEC, SET) zwraca USER_READ | USER_EXEC

  • void print(enum permissions perm); - wypisujący podsumowanie stanu uprawnień

  • enum permissions get(char const *path); - pobierający uprawnienia z pliku

    1. dla pliku foo.c, mającego uprawnienia 644 (ósemkowo): get("foo.c") zwraca USER_READ | USER_WRITE | GROUP_READ | OTHER_READ

  • void set(char const *file, enum permissions perm); - nadający wyliczone uprawnienia plikowi

W powyższych przykładowych deklaracjach i wywołaniach założono istnienie dwóch typów wyliczeniowych:

  • enum action opisującego możliwe tryby zmiany uprawnień (+ - ADD, - - DEL, = - SET)

  • enum permissions opisującego możliwe uprawnienia z podziałem na 3 typy: użytkownika (USER_WRITE, USER_READ, USER_EXEC), grupy (GROUP_WRITE, GROUP_READ, GROUP_EXEC), pozostałych (OTHER_WRITE, OTHER_READ, OTHER_EXEC).

enum permissions może odpowiadać bezpośrednio wartościom oczekiwanym przez funkcję systemową chmod; podobnie wartości enum action mogą odpowiadać wartościom ascii znaków +-=. Ułatwi to implementację programu przez bezpośrednie modelowanie wejścia i wyjścia.

Każda z funkcji, która operuje z systemem operacyjnym (get, set) oraz może przyjąć błędne dane (parse) powinna wypisać błąd i zakończyć działanie programu.

Implementacja programu jest wtedy odpowiednim złożeniem funkcji:

int main(int argc, char **argv)
{
	// Sprawdzenie czy liczba argumentów się zgadza

	enum permissions perm;
	enum action action;

	parse(argv[1], &perm, &action);

	for (int i = 2; i < argc; ++i) {
		enum permissions p = apply(get(argv[i]), perm, action);
		set(argv[i], p);
		printf("%s:", argv[i]);
		print(p);
	}

	return 0;
}

errno

errno jest globalną zmienną wykorzystywaną do przechowywania kodów błędów zwracanych przez funkcje biblioteki standardowej C oraz przez funkcje realizujące wywoływanie wywołań systemowych. Dostępna jest w pliku nagłówkowym <errno.h>.

$ cat example.c
#include <errno.h>
#include <stdio.h>
#include <string.h>

int main()
{
	FILE *f = fopen("/path/to/file/that/doesnt/exists", "r");
	if (f) {
		printf("File exists\n");
		fclose(f);
	} else {
		printf("errno = %d\n", errno);
		printf("strerror(errno) = %s\n", strerror(errno));
		perror("perror(...)");
	}
	return 0;
}
$ cc -o example example.c
$ ./example
errno = 2
strerror(errno) = No such file or directory
perror(...): No such file or directory

Komenda errno -l pozwala na wypisanie możliwych wartości zmiennej errno (niestety nie dostępna na wydziale). Każdy wątek posiada własną zmienną errno.

$ errno -l | head -n5
EPERM 1 Operation not permitted
ENOENT 2 No such file or directory
ESRCH 3 No such process
EINTR 4 Interrupted system call
EIO 5 Input/output error

Uprawienia w systemie Linux

System Linux kontroluje dostęp do plików przez system uprawnień. Dla każdego pliku możemy kontrolować możliwość zapisu (w), odczytu (r) i wykonania (x) oraz kto daną możliwość ma: użytkownik, do którego należy dany plik (u), grupa do której należy dany plik (g) oraz pozostali użytkownicy systemu (o).

Aby zmienić uprawnienia pliku używamy komendy chmod, aby zmienić użytkownika lub grupę posiadającą plik chown, aby wypisać uprawnienia możemy użyć komendy stat lub ls -l.

$ touch a
$ ls -l a
-rw-r--r-- 1 anon anon 0 kwi 14 15:18 a
$ stat a
  File: a
  Size: 0               Blocks: 0          IO Block: 4096   regular empty file
Device: 0,34    Inode: 581         Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1000/   anon)   Gid: ( 1000/   anon)
Access: 2025-04-14 15:18:45.974560933 +0200
Modify: 2025-04-14 15:18:45.974560933 +0200
Change: 2025-04-14 15:18:45.974560933 +0200
 Birth: 2025-04-14 15:18:45.974560933 +0200
$ ./a
bash: ./a: Permission denied
$ chmod u+x a
$ ls -l a
-rwxr--r-- 1 anon anon 0 kwi 14 15:18 a
$ ./a
$ cat a
$ chmod u-rwx a
$ cat a
cat: a: Permission denied
$ ls -l a
----r--r-- 1 anon anon 0 kwi 14 15:18 a
$ rm a
rm: remove write-protected regular empty file 'a'? y

Funkcja chmod

Plik nagłówkowy: <sys/stat.h>, dokumentacja: chmod(2)

int chmod(const char *pathname, mode_t mode);

Przykład użycia: nadania uprawnień 644 plikowi test.c (odczyt i zapis dla użytkownika, odczyt dla grupy i pozostałych):

#include <stdio.h>
#include <sys/stat.h>

int main()
{
	if (chmod("test.c", 0644) < 0) {
		perror("chmod");
		return 1;
	}
	return 0;
}

Funkcja stat

Plik nagłówkowy: <sys/stat.h>, dokumentacja: stat(2), stat(3type)

struct stat {
	mode_t st_mode;
	...
};

int stat(const char *restrict pathname, struct stat *restrict statbuf);

Przykład odczytu uprawnień pliku test.c:

#include <stdio.h>
#include <sys/stat.h>

int main()
{
	struct stat s;
	if (stat("test.c", &s) < 0) {
		perror("stat");
		return 1;
	}

	printf("test.c perm decimal: %d\n", s.st_mode);
	printf("test.c perm octal:   %o\n", s.st_mode);

	printf("test.c perm binary:  ");
	for (int i = 3 * 3 - 1; i >= 0; --i) {
		printf("%d", (s.st_mode & (1 << i)) > 0 ? 1 : 0);
	}
	printf("\n");

	return 0;
}

Typy wyliczeniowe (enum)

Typ wyliczeniowy pozwala na stworzenie dedykowanego typu liczbowego dla zbioru stałych (zazwyczaj jednego rodzaju).

Przykład:

#include <stdio.h>
#include <assert.h>

enum weekday
{
	MONDAY = 1,
	TUESDAY, /* = 2, domyślnie ma wartość poprzednią + 1 */
	WEDNESDAY,
	THURSDAY,
	FRIDAY,
	SATURDAY,
	SUNDAY,

	MIN_WEEKDAY = MONDAY,
	MAX_WEEKDAY = SUNDAY,
};

char const* pretty(enum weekday wd)
{
	switch (wd) {
	case MONDAY:    return "poniedziałek";
	case TUESDAY:   return "wtorek";
	case WEDNESDAY: return "środa";
	case THURSDAY:  return "czwartek";
	case FRIDAY:    return "piątek";
	case SATURDAY:  return "sobota";
	case SUNDAY:    return "niedziela";
	}
	static_assert(MAX_WEEKDAY - MIN_WEEKDAY == 6, "Number of days in a week has changed");
}

int main()
{
	enum weekday wd;

	do {
		printf("Wybierz dzień tygodnia: ");
		scanf("%d", &wd);
	} while (wd < MIN_WEEKDAY || wd > MAX_WEEKDAY);

	printf("Wybrano dzień tygodnia: %s\n", pretty(wd));
	return 0;
}

Flagi bitowe

Przykład:

#include <stdio.h>
#include <assert.h>

enum when
{
	ON_MONDAY    = 0b0000001,
	ON_TUESDAY   = 0b0000010,
	ON_WEDNESDAY = 0b0000100,
	ON_THURSDAY  = 0b0001000,
	ON_FRIDAY    = 0b0010000,
	ON_SATURDAY  = 0b0100000,
	ON_SUNDAY    = 0b1000000,

	ON_WORKDAY = ON_MONDAY | ON_TUESDAY | ON_WEDNESDAY | ON_THURSDAY | ON_FRIDAY,
	ON_WEEKEND = ON_SATURDAY | ON_SUNDAY,
	EVERYDAY = ON_WORKDAY | ON_WEEKEND,
};

struct todo
{
	enum when when;
	char const *what;
};

struct todo todos[] = {
	{ .when = EVERYDAY, .what = "Drink water" },
	{ .when = ON_MONDAY, .what = "Go to the gym" },
	{ .when = ON_WEEKEND, .what = "Relax" },
};

int main()
{
	enum when today = ON_MONDAY;

	for (int i = 0; i < sizeof(todos) / sizeof(*todos); ++i) {
		struct todo todo = todos[i];
		if ((todo.when & today) == today) {
			printf("%s\n", todo.what);
		}
	}

}

X-makra

Pozwalają na ułatwienie definicji funkcji wykorzystujących wyliczenia:

#include <stdio.h>
#include <assert.h>

#define WEEKDAY_ENUM(X) \
	X(MONDAY,    1, "poniedziałek") \
	X(TUESDAY,   2, "wtorek")       \
	X(WEDNESDAY, 3, "środa")        \
	X(THURSDAY,  4, "czwartek")     \
	X(FRIDAY,    5, "piątek")       \
	X(SATURDAY,  6, "sobota")       \
	X(SUNDAY,    7, "niedziela")

enum weekday
{
#define X(label, number, name) label = number,
WEEKDAY_ENUM(X)
#undef X

	MIN_WEEKDAY = MONDAY,
	MAX_WEEKDAY = SUNDAY,
};

char const* pretty(enum weekday wd)
{
	switch (wd) {
#define X(label, number, name) case label: return name;
WEEKDAY_ENUM(X)
#undef X
	}
	static_assert(MAX_WEEKDAY - MIN_WEEKDAY == 6, "Number of days in a week has changed");
}

int main()
{
	enum weekday wd;

	do {
		printf("Wybierz dzień tygodnia: ");
		scanf("%d", &wd);
	} while (wd < MIN_WEEKDAY || wd > MAX_WEEKDAY);

	printf("Wybrano dzień tygodnia: %s\n", pretty(wd));
	return 0;
}

Wyniki ankiety

Stan z 2025-04-14 14:50: odpowiedziały dwie osoby.

Należy powtórzyć (lista uporządkowana wg priorytetu):

  1. wyliczenia

  2. proces kompilacji i linkowania

  3. tablice dynamiczne

  4. struktury i unie

  5. struktury danych

Na zajęciach brakuje:

  • przykładowych rozwiązań zadań domowych i ich omówienia na zajęciach.

  • szkieletu rozwiązania zadania domowego, szczególnie dla większych zadań (stos, BST, lista)

  • omówienia funkcji, które mamy wykorzystać w zadaniach

  • wskazówki jak korzystać z tak zaawanasowanej dokumentacji jak en.cppreference.com

  • brak notatek z zagadnień związanych z architekturą komputera (np. w formie linków do dalszych materiałów)

Oceny formy zajęć: pozytywne, podkreślono wartość dodatkowego kontekstu dawanego przez osobę prowadzącą