Jazyk C 2013 – 7. cvičenie [Spracovanie /etc/passwd]

Textové súbory

Výpis textového súboru

Vypíšte obsah textového súboru na štandardný výstup. Predpokladajte, že každý riadok má dĺžku maximálne 128 znakov. Názov súboru nech je zadrôtovaný v súbore cez #define.

Riadkovo-orientované súbory načítavajme cez fgets(). Tá potrebuje:

  • reťazec (buffer), do ktorého načíta znaky
  • počet znakov, ktorý sa má načítať
  • vstupný súbor

Príklad:

fgets(riadok, 128, subor);

Načítavanie skončí, ak:

  • načíta sa 127 znakov,
  • alebo sa načíta znak konca riadka \n, ktorý sa zapíše do buffera
  • alebo sa naďabí na koniec súboru.

Funkcia potom ukončí reťazec znakom \0.

Funkcia vráti NULL, ak nastane chyba, alebo sa narazí na koniec súboru.

Jednoduchý príklad:

#include<stdio.h>

#define SUBOR "/etc/passwd"

int main()
{
        FILE * subor;
        char buf[128];

        subor = fopen(SUBOR, "r");
        if(subor == NULL) {
                perror("Subor " SUBOR " sa nenasiel");
                return -1;
        }

        while(fgets(buf, sizeof(buf), subor) != NULL) {
                puts(buf);
        }

        fclose(subor);

        return 0;
}

Výsledkom bude:

root:x:0:0:root:/root:/bin/bash

daemon:x:1:1:daemon:/usr/sbin:/bin/false

bin:x:2:2:bin:/bin:/bin/false

sys:x:3:3:sys:/dev:/bin/false

teda očakávané riadky… ibaže medzi každým z nich bude medzera. Problém je spôsobený dvojitým znakom konca riadka \n˙:

  • jeden koniec riadka sa nachádza v reťazci v bufferi: fscanf() totiž načítava riadok vrátane koncového znaku
  • a druhý koniec riadka sa vo výsledku objaví vďaka puts(), ktoré svoj parameter vytlačí a ukončí ho znakom \n. V Jave by to bola analógia príkazu System.out.println("root:x:0:0:root:/root:/bin/bash").

Namiesto puts(buf) tak môžeme skúsiť aj analógiu

printf("%s", buf);

Pozor! Nikdy netlačte reťazec cez

printf(buf);

Prvý parameter funkcie printf() je formátovací reťazec. Ak sa premenná buf stane formátovacím reťazcom, a navyše jej obsah príde zvonku (= zo súboru), niekto ju môže zneužiť.

Napríklad uvedie do súboru jediný riadok s názvom %s.

V tom prípade bude printf() čakať ďalší argument a ak ho nedostane, program spadne.

Výpis /etc/passwd

Zo linuxového súboru s údajmi o používateľoch /etc/passwd vypíšte len tie riadky, ktoré patria používateľovi so zadaným UID.
Súbor obsahuje položky oddelené dvojbodkami
* login používateľa (maximálne 32 znakov) * indikátor uloženia hesla v externom súbore /etc/shadow. Vždy má hodnotu x. * ID používateľa (UID), jednoznačné vzhľadom na celý súbor. Prirodzené číslo. * ID skupiny používateľa (GID). Prirodzené číslo. * položku Gecos s textovým komentárom o používateľovi. Obvykle obsahuje podpoložky oddelené čiarkami, zväčša s kontaktnými informáciami o používateľovi * cestu k domovskému adresáru * program, ktorý sa má spustiť po prihlásení do systému: typicky je ním niektorý shell (napr. bash)

Riešenie

  • položiek je veľa, ale zaujímajú nás len prvá (login) a tretia (UID).
  • predpokladajme zatiaľ, že aj UID je reťazec
  • využime funkciu strtok(), ktorá tokenizuje reťazec, teda rozsekáva ho na podreťazce (tokeny) podľa špecifikovaného oddeľovača:
    • prvý parameter je typu char *, teda reťazec, ktorý má byť rozdeľovaný
    • druhý parameter je typu char *, obsahuje oddeľovače, ktorými sa bude reťazec deliť
    • funkcia vracia char *, čo možno chápať ako reťazec s príslušnou podpoložkou (teda token)
      • nezabúdame, že reťazec je pole znakov ukončené \0 a teda ekvivalentné s pointerom na prvý znak.
  • strtok() sa používa v dvoch fázach:
    • vo 1. fáze zadáme reťazec, ktorý sa má rozsekať, a vráti sa prvý token
    • v 2. fáze dávame namiesto vstupného podreťazca NULL a strtok() vráti druhý token
    • v 3., a ďalšej fáze dávame namiesto vstupného podreťazca opäť NULL a strtok() vráti tretí, atď. token
  • strtok() modifikuje reťazec na vstupe! Majme reťazec

    root:x:0
    

    ktorý začína v pamäti na adrese 112. V skutočnosti je reprezentovaný takto:

    @112  @113  @114  @115  @116  @117  @118  @119  @120
    'r' | 'o' | 'o' | 't' | ':' | 'x' | ':' | '0' | '\0'
    

    Po prvom zavolaní token = strtok(vstup, ":") sa “poorie” reťazec: funkcia nájde oddeľovač a nahradí ho znakom \0 a vráti pointer na začiatok reťazca

    @112  @113  @114  @115  @116   @117  @118  @119  @120
    'r' | 'o' | 'o' | 't' | '\0' | 'x' | ':' | '0' | '\0'
    

    V premennej token sa tak objaví adresa 112.

    Všimnime si, ako sa token správa ako korektný reťazec: je totiž ukončený \0! (Toto je majstertrik funkcie!)

    Po druhom zavolaní s parametrom NULL, teda token = strtok(NULL, ":") sa ďalej orie reťazec: funkcia nájde ďalší výskyt oddeľovača a nahradí ho znakom \0 a vráti pointer na začiatok tokenu

    @112  @113  @114  @115  @116   @117  @118   @119   @120
    'r' | 'o' | 'o' | 't' | '\0' | 'x' | '\0' | '0' | '\0'
    

    V premennej token sa tak objaví adresa 117.

    Po treťom zavolaní (opäť s parametrom NULL) funkcia hľadá ďalší výskyt oddeľovača, ibaže .. v tomto reťazci už žiadny ďalší oddelovač neexistuje. Tretí token teda bude siahať až po koniec reťazca.

    V premennej token sa tak objaví adresa 119.

  • dôležité: strtok() zdemoluje vstupný reťazec!

    • ak chceme po skončení tokenizovania pristupovať k pôvodnému vstupu, nedá sa to!
    • reťazec vstup z príkladu je totiž pointer na prvý znak, teda obsahuje adresu 112.
    • keďže na adrese 116 bol oddeľovač prepísaný koncovým znakom \0, reťazec vstup je stotožnený s prvým tokenom
  • vstupný reťazec môžeme odložiť do pomocnej premennej: priradením reťazcov

    • priraďovanie reťazcov nefunguje cez =!
      • reťazec je totiž pole znakov ukončené nulou, a v našom prípade je to ekvivalentné s pointerom na prvý prvok
      • priradenie teda priradzuje len adresy, ale nie ich obsahy
      • mali by sme tak dve premenné s rovnakou adresou a problém orania reťazca funkciou strtok() by sme nevyriešili
    • na priradenie reťazcov potrebujeme teda dva nezávislé reťazce, teda dve polia
    • priradenie reťazca je vlastne kopírovanie obsahu polí
    • využime funkciu strncpy()

      char *strncpy(char *ciel, const char *zdroj, size_t pocet_znakov);
      

      Napr:

      strncpy(riadok2, riadok, MAX_DLZKA_RIADKA * sizeof(char) );
      

      Keďže dĺžka premennej riadok a riadok2 je rovnaká, funkcia prebehne bez problémov.

      • Pozor v okrajových prípadoch:
        • ak je cieľ kratší než zdroj, prepisujeme cudziu pamäť
        • pri uvádzaní počtu kopírovaných znakov (počet krát veľkosť charu) nezabudnime na \0!
        • funkcia neukončuje kopírovanie znakom \0! Ak máme zdroj 'C', 'a', 'u', '!', '\0' a kopírujeme len 3 znaky, v koncovom poli sa objaví 'C', 'a', 'u', čo je pokazený reťazec!
  • na porovnanie hľadaného UID využime porovnávanie reťazcov

    • rozhodne nefunguje porovnanie cez ==!
      • v Jave to takto tiež nefunguje.
      • reťazec je totiž pole znakov ukončené nulou, a v našom prípade je to ekvivalentné s pointerom na prvý prvok
      • cez == by sme porovnávali dva pointery, teda dve adresy, teda zisťovali, či sú reťazce na rovnakom mieste v pamäti.
    • na porovnanie reťazcov slúži strcmp(): funkcia vracia nulu, ak sa reťazce rovnajú

      if( strcmp(r1, r2) == 0 )
      
    • niekde vidieť aj obrátený zápis spoliehajúci sa na fakt, že “v C je 0 považovaná za false“:

      if( !strcmp(r1, r2) )
      

Celý zdroják

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

#define SUBOR "/tmp/passwd"

#define HLADANE_ID "1134"

#define MAX_DLZKA_RIADKA 128

#define SEPARATOR ":"

int main() {
        FILE * subor;
        char riadok[MAX_DLZKA_RIADKA];
        char cely_riadok[MAX_DLZKA_RIADKA];

        char * token;

        subor = fopen(SUBOR, "r");
        if(subor == NULL) {
                perror("Subor " SUBOR " sa nenasiel");
                return -1;
        }

        while( fgets(riadok, MAX_DLZKA_RIADKA, subor) != NULL ) {
                strncpy(cely_riadok, riadok, MAX_DLZKA_RIADKA * sizeof(char) );

                if(strcmp(riadok, "\n") == 0) {
                        fprintf(stderr, "Prazdny riadok!");
                        continue;
                }

                token = strtok(riadok, SEPARATOR);
                if(token == NULL) {
                        fprintf(stderr, "Chyba meno pouzivatela");
                        continue;
                }
                token = strtok(NULL, SEPARATOR);
                if(token == NULL) {
                        fprintf(stderr, "Chyba 'x'");
                        continue;
                }
                token = strtok(NULL, SEPARATOR);
                if(token == NULL) {
                        fprintf(stderr, "Chyba UID pouzivatela");
                        continue;
                }

                if(strcmp(HLADANE_ID, token) == 0) {
                        puts(cely_riadok);
                }
        }

        fclose(subor);

        return 0;
}

Pridaj komentár

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *