#+title: Curso Básico da Linguagem C
#+subtitle: Aula 11: Leitura da entrada padrão com 'scanf'
#+author: Blau Araujo
#+startup: show2levels
#+options: toc:3

* Aula 11: Leitura da entrada padrão com 'scanf'

[[https://youtu.be/MZiI95b2gdY][Vídeo desta aula]]

A entrada padrão (/stream/ =stdin= ou descritor de arquivos =0=) pode estar ligada...

- A um terminal para leitura interativa (digitação);
- A um arquivo via redirecionamento de leitura;
- À saída de um processo via pipe ou arquivo FIFO;
- A um arquivo de forma programática (ex: funções =freopen=, =dup2=).

Para verificar se o fluxo =stdin= está ligado a um terminal, nós podemos
utilizar a função =isatty= (=unistd.h=):

#+begin_src c
int isatty(int fd);
#+end_src

Onde =int fd= é o descritor de arquivos testado. O retorno será o inteiro =1=,
se =fd= estiver ligado a um terminal, ou =0=, se estiver ligado a um pipe ou
redirecionado para um arquivo.

Exemplo (=test-tty.c=):

#+begin_src c
#include <stdio.h>
#include <unistd.h>

int main(void) {
    if (isatty(0)) {
        puts("STDIN ligada ao terminal.");
    } else {
        puts("STDIN ligada a um arquivo ou pipe.");
    }

    return 0;
}
#+end_src

Compilação e testes:

#+begin_example
:~$ gcc -Wall test-tty.c
:~$ ./a.out
STDIN ligada ao terminal.
:~$ ./a.out < /dev/null
STDIN ligada a um arquivo ou pipe.
:~$ : | ./a.out
STDIN ligada a um arquivo ou pipe.
#+end_example

#+begin_quote
O comando interno do shell =:= é o comando nulo, que não faz nada e sempre termina
com sucesso.
#+end_quote

Para associar programaticamente a entrada padrão a um arquivo (de nome recebido
como argumento, por exemplo), nós podemos utilizar a função =freopen= para abrir
um arquivo para leitura através de um fluxo (/stream/) previamente aberto: no caso
o stream =stdin=...

#+begin_src c
FILE *freopen(const char *caminho, const char *modo, FILE *stream);
#+end_src

Onde:

- =caminho=: string do nome do arquivo;
- =modo=: string de modo de abertura (="r"=, ="w"=, ="rw"=);
- =stream=: um fluxo de dados previamente aberto.

Retorna um ponteiro para o tipo =FILE= ou =NULL=, em caso de erro. 

#+begin_quote
O propósito original desta função é alterar o arquivo associado a um dos
fluxos padrão (=stdin=, =stdout= ou =stderr=).
#+end_quote

Exemplo (=redir-stdin.c=):

#+begin_src c
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {

    // Diretório 'fd' do processo de 'a.out'...
    puts("Antes do redirecionamento...");
    system("ls -l /proc/$(pidof a.out)/fd");

    // Nome do arquivo...
    char *filename;
    if (argc > 1) {
        // Primeiro argumento...
        filename = argv[1];
    } else {
        // Dispositivo nulo...
        filename = "/dev/null";
    }

    // Redirecionamento...
    FILE* stream = freopen(filename, "r", stdin);
    if (!stream) {
        perror("freopen");
        return EXIT_FAILURE;
    }

    // Diretório 'fd' do processo de 'a.out'...
    puts("Depois do redirecionamento...");
    system("ls -l /proc/$(pidof a.out)/fd");

    return EXIT_SUCCESS;
}
#+end_src

Compilação...

#+begin_example
:~$ gcc -Wall redir-stdin.c
#+end_example

Executando sem argumentos (redireciona para =/dev/null=):

#+begin_example
:~$ ./a.out
Antes do redirecionamento...
total 0
lrwx------ 1 blau blau 64 abr 24 09:16 0 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 2 -> /dev/pts/0
Depois do redirecionamento...
total 0
lr-x------ 1 blau blau 64 abr 24 09:16 0 -> /dev/null
lrwx------ 1 blau blau 64 abr 24 09:16 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 2 -> /dev/pts/0
#+end_example

Executando com argumentos (nome do arquivo no primeiro argumento):

#+begin_example
:~$ ./a.out /etc/shells
Antes do redirecionamento...
total 0
lrwx------ 1 blau blau 64 abr 24 09:16 0 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 2 -> /dev/pts/0
Depois do redirecionamento...
total 0
lr-x------ 1 blau blau 64 abr 24 09:16 0 -> /etc/shells
lrwx------ 1 blau blau 64 abr 24 09:16 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 2 -> /dev/pts/0
#+end_example

No caso de um erro de abertura (ex: arquivo "banana" não existe):

#+begin_example
:~$ ./a.out banana
Antes do redirecionamento...
total 0
lrwx------ 1 blau blau 64 abr 24 09:21 0 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:21 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:21 2 -> /dev/pts/0
freopen: No such file or directory
#+end_example

** Leitura interativa da entrada padrão

Seja o conteúdo de um arquivo ou uma entrada digitada no terminal, a leitura
da entrada padrão pode ser feita com várias formas e com diversas funções da
biblioteca padrão:

- Leitura de caracteres: =getchar=, =getc(stdin)= e =fgetc(stdin)=;
- Leitura de linhas (até =\n=): =fgets(buf, size, stdin)=, =getline(&buf, &size, stdin)=;
- Leitura formatada de linhas: =scanf=, =fscanf(stdin, ...)=; 

Mas, como a leitura das linhas de um arquivo é mais controlada (o conteúdo
é previamente conhecido), nós vamos concentrar nossa atenção na leitura de
linhas digitadas no terminal por um usuário (leitura interativa). Para isso,
nós estudaremos três funções (nesta e nas próximas aulas):

- Leitura formatada de linhas: =scanf=;
- Leitura de linhas: =fgets=;
- Leitura de caracteres: chamada de sistema =read=.

** Leitura formatada de linhas com 'scanf'

A função =scanf= lê os bytes de uma linha enquanto encontra casamentos com os
especificadores definidos em uma string de formato (como no =printf=). A parte
da linha que encontrar casamento é passada para um ou mais endereços de
variáveis (ou endereços em ponteiros) e tudo que vier depois permanece no
buffer de entrada do processo.

Outros pontos importantes:

- Cada especificador de formato é casado com apenas uma palavra da linha
  lida (palavras são cadeias de caracteres delimitadas por espaços,
  tabulações e quebras de linha).
- Retorna o número de casamentos com as especificações de formatos que
  foram encontrados e atribuídos (o que pode ser menor do que a quantidade
  de especificadores fornecidos).
- Em caso de erro, retorna zero ou =EOF= (com vários significados possíveis).
- Altera a variável =errno= (=errno.h=), o que possibilita obter descrições de
  erros, por exemplo, com as funções =perror= (=stdio.h=) ou =strerror= (=string.h=).
- No caso da formatação como string, o caractere nulo (='\0'=) é inserido
  após o último byte do casamento encontrado.
- Tudo que delimita a quantidade de bytes lidos é a forma como a string
  de formato é construída.
- Uma string de formato mal construída pode levar a vulnerabilidades de
  memória.

A função =scanf= é considerada insegura por muitos programadores, mas a
verdade é que ela é reconhecidamente difícil de ser utilizada corretamente.

De =man 3 scanf=:

#+begin_quote
It is very difficult to use these functions correctly, and it is preferable
to read  entire  lines with fgets(3) or getline(3) and parse them later with
sscanf(3) or more specialized functions such as strtol(3).
#+end_quote

Ou seja, o próprio manual diz que é muito difícil utilizar corretamente
as funções da família do =scanf= e pode ser mais fácil ler linhas inteiras
com funções como =fgets= e =getline= e fazer os processamentos necessários
com a função =sscanf= ou outras funções mais especializadas em conversões
de cadeias de caracteres em valores numéricos.

*** Para reduzir as chances de erro

A maior parte dos problemas com =scanf= decorre da conversão de strings.
Por isso, é sempre bom atentar para alguns detalhes, como:

- Sempre validar o retorno de =scanf= e fazer o tratamento dos erros.
- Na leitura de uma linha inteira como string (por exemplo, uma linha
  que contenha apenas uma palavra), sempre limitar a quantidade de
  caracteres lidos no especificador =%s= (ex: =%9s= para ler até 9 bytes).
- Na leitura de caracteres únicos, o especificador =%c= deve ser precedido
  de um espaço.


*** Exemplos

**** Leitura de duas strings

#+begin_src c
char str1[10];
char str2[10];

// "%Ns" não deve incluir '\0'! 
scanf("%9s", str1);
scanf("%9s", str2);
#+end_src

**** Separando a entrada digitada em campos

#+begin_src c
char str1[10];
char str2[10];

scanf("%9s %9s", str1, str2);
    
printf("str1: %s\n", str1);
printf("str2: %s\n", str2);
#+end_src

**** Lendo caracteres

Neste caso, apenas o primeiro caractere será casado, seja ele qual for.

#+begin_src c
printf("Confirma (s/n)? ");
char reply;
scanf("%c", &reply);
#+end_src

Mas, se o usuário digitar espaços antes dos caracteres esperados,
o primeiro espaço é que será consumido. Para evitar isso...

#+begin_src c
scanf(" %c", &reply);
#+end_src

**** Lendo vários caracteres

#+begin_src c
char a, b, c;
scanf(" %c %c %c", &a, &b, &c);
printf("A:%c B:%c C:%c\n", a, b, c);
#+end_src

**** Lendo e formatando números

Inteiros:

#+begin_src c
printf("Digite um inteiro: ");
int num;
scanf("%d", &num);
#+end_src

Ponto flutuante:

#+begin_src c
printf("Digite um float: ");
float f;
int c = scanf("%f", &f);
#+end_src

*** Descarregando o buffer de entrada

A função =scanf= não consome nada que não case com o formato especificado,
o que inclui a quebra de linha. Todo o restante da linha permanece no buffer
de entrada e, na ausência de uma função da biblioteca padrão para descarregá-lo,
nós podemos criar uma função como esta:

#+begin_src c
void stdin_flush(void) {
    char ch;
    while ((ch = getchar()) != '\n' && ch != EOF);
}
#+end_src

Ela consumiria todos os caracteres restantes no buffer de entrada enquanto
o caractere atribuído a =ch= fosse diferente da quebra de linha (=\n=) ou de =-1=,
que é o valor retornado por =getchar= quando tenta ler caracteres após o fim
de um arquivo (constante simbólica =EOF=).

Isso é particularmente necessário quando houver leituras consecutivas da
entrada padrão com =scanf=...

#+begin_src c
int a;
scanf("%d", &b);
stdin_flush();

int b;
scanf("%d", &b);
stdin_flush();

/* ... */
#+end_src

*** Inicialização de valores numéricos

Nos exemplos anteriores, nós temos um problema que precisa ser considerado:
a leitura pode falhar e, nesse caso, nada será escrito nos endereços das
variáveis passados para =scanf=. Como as variáveis não foram inicializadas,
seu conteúdo será /lixo de memória/. Portanto, é importante que essas variáveis
sejam devidamente inicializadas com algum valor, por exemplo:

#+begin_src c
int a = 0;
scanf("%d", &b);
stdin_flush();

int b = 0;
scanf("%d", &b);
stdin_flush();

/* ... */
#+end_src

Este problema é relevante com variáveis que recebem valores numéricos (=char=,
=int=, =float=, =double=, etc), porque, em geral, elas são utilizadas em cálculos
e até o lixo de memória pode ser tratado como número válido, levando a
resultados enganosos.

*** Buffer overflow

A leitura de strings com =scanf= é bastante suscetível a causar vulnerabilidades
graves de memória, como o /buffer overflow/, que é quando a quantidade de dados
escritos a partir do endereço de um buffer excedem seu limite máximo e continuam
sendo escritos em endereços válidos subsequentes.

#+begin_quote
Quando o programa tentar escrever os bytes excedentes em uma região inválida
da memória, nós temos uma /falha de segmentação/.
#+end_quote

Observe este exemplo:

#+begin_src c
char ref[10] = "123456789"; // Uma string qualquer.
char str[10];               // Buffer para a stringa digitada no terminal.

scanf("%s", str);           // Especificador %s sem limite.

printf("str: %s\n", str);
printf("ref: %s\n", ref);
#+end_src

Ao ser compilado e executado, os 10 bytes de =ref= serão escritos na pilha
e, acima deles (em um endereço 10 bytes mais baixo), serão reservados os
10 bytes para o buffer =str=.

Se o usuário digitar =abc=, esta parte da pilha ficará assim:

#+begin_example
    +------+------+------+------+------+------+------+------+------+------+
ref | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x00 |
    +------+------+------+------+------+------+------+------+------+------+
      0x0A   0x0B   0x0C   0x0D   0x0E   0x0F   0x10   0x11   0x12   0x13
      
    +------+------+------+------+------+------+------+------+------+------+
str | 0x61 | 0x62 | 0x63 | 0x00 | lixo | lixo | lixo | lixo | lixo | lixo |
    +------+------+------+------+------+------+------+------+------+------+
      0x00   0x01   0x02   0x03   0x04   0x05   0x06   0x07   0x08   0x09 
#+end_example

A impressão no terminal será:

#+begin_example
str: abc
ref: 123456789
#+end_example

Mas, se digitar =abcdefghjij=, este será o resultado:

#+begin_example
    buffer overflow
       ↓
    +------+------+------+------+------+------+------+------+------+------+
ref | 0x00 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x00 |
    +------+------+------+------+------+------+------+------+------+------+
      0x0A   0x0B   0x0C   0x0D   0x0E   0x0F   0x10   0x11   0x12   0x13
      
    +------+------+------+------+------+------+------+------+------+------+
str | 0x61 | 0x62 | 0x63 | 0x64 | 0x65 | 0x66 | 0x67 | 0x68 | 0x69 | 0x6A |
    +------+------+------+------+------+------+------+------+------+------+
      0x00   0x01   0x02   0x03   0x04   0x05   0x06   0x07   0x08   0x09 
#+end_example

E isso vai imprimir:

#+begin_example
str: abcdefghij
ref: 
#+end_example

Porque o caractere nulo (=0x00=) foi escrito no décimo primeiro endereço
após o endereço de =str= (endereço =0x0A=), que é o primeiro byte de =ref=.

Esse problema poderia ser evitado especificando o limite do tamanho da
string:

#+begin_src c
char ref[10] = "123456789"; // Uma string qualquer.
char str[10];               // Buffer para a stringa digitada no terminal.

scanf("%9s", str);           // Limite de 9 bytes para a string.

printf("str: %s\n", str);
printf("ref: %s\n", ref);
#+end_src

Executando e digitando =abcdefghij=, os dados na pilha seriam...

#+begin_example
    +------+------+------+------+------+------+------+------+------+------+
ref | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x00 |
    +------+------+------+------+------+------+------+------+------+------+
      0x0A   0x0B   0x0C   0x0D   0x0E   0x0F   0x10   0x11   0x12   0x13
      
    +------+------+------+------+------+------+------+------+------+------+
str | 0x61 | 0x62 | 0x63 | 0x64 | 0x65 | 0x66 | 0x67 | 0x68 | 0x69 | 0x00 |
    +------+------+------+------+------+------+------+------+------+------+
      0x00   0x01   0x02   0x03   0x04   0x05   0x06   0x07   0x08   0x09 
#+end_example

E nós veríamos isso no terminal:

#+begin_example
str: abcdefghi    <- O 'j' não foi consumido...
ref: 123456789
#+end_example


