INF1018 - Software Básico (2017.1)
Primeiro Trabalho

Conversão entre codificações UNICODE

O objetivo deste trabalho é implementar, na linguagem C, duas funções (utf8_16 e utf16_8), que recebem como entrada um arquivo contendo um texto codificado em um formato UNICODE (UTF-8 ou UTF-16) e geram como saida um arquivo contendo o mesmo texto, codificado no outro formato.

Instruções Gerais


Leia com atenção o enunciado do trabalho e as instruções para a entrega. Em caso de dúvidas, não invente. Pergunte!
  • O trabalho deve ser entregue até meia-noite (23:59) do dia 28 de abril.
  • Trabalhos entregues com atraso perderão um ponto por dia de atraso.
  • Trabalhos que não compilem (isto é, que não produzam um executável) não serão considerados! Ou seja, receberão grau zero.

  • Os trabalhos devem preferencialmente ser feitos em grupos de dois alunos .
  • Alguns grupos poderão ser chamados para apresentações orais / demonstrações dos trabalhos entregues.


  • Codificação UNICODE

    Em computação, caracteres de texto são tipicamente representados por códigos especificados por algum padrão de codificação. Um padrão bastante conhecido é a codificação ASCII, que utiliza valores inteiros de 0 a 127 para representar letras, dígitos e alguns outros simbolos. Algumas extensões dessa codificação utilizam também a faixa de valores de 128 a 255 para representar, por exemplo, caracteres acentuados e alguns outros símbolos adicionais.

    A codificação ASCII e outros padrões de codificação que utilizam códigos de um único byte são limitados à representação de apenas 256 símbolos diferentes. Para permitir a representação de um conjunto maior de caracteres foi criada, no final da década de 1980, a codificação UNICODE. A versão corrente dessa codificação é capaz de representar os caracteres utilizados por todos os idiomas conhecidos, além de diversos outros símbolos.

    Cada caractere em UNICODE é associado a um código na faixa de 0 a 0x10FFFF, o que permite a representação de 1.114.112 símbolos diferentes. Na notação adotada pelo padrão UNICODE, U+xxxx identifica o código com valor hexadecimal xxxx. Por exemplo, o código U+00A9 (o código do símbolo ©) corresponde ao valor hexadecimal 0x00A9.

    Existem algumas formas diferentes de codificação de caracteres UNICODE. Para este trabalho, as codificações de interesse são a UTF-8 e a UTF-16.

    Codificação UTF-8

    Na codificação UTF-8, os códigos dos caracteres são representados em um número variável de bytes. O tamanho mínimo utilizado para representar um caractere em UTF-8 é um byte (8 bits); se a representação necessita de mais espaço, mais bytes são utilizados (até o máximo de 4 bytes).

    Uma característica importante é que a codificação UTF-8 é compatível com o padrão ASCII, ou seja, os 128 caracteres associados aos códigos de 0 a 0x7F em ASCII tem a mesma representação (em um único byte) em UTF-8.

    A tabela a seguir indica para cada faixa de valores de códigos UNICODE o número de bytes necessários para representá-los e a codificação usada para essa faixa.

    Código UNICODE Representação UTF-8 (byte a byte)
    U+0000 a U+007F0xxxxxxx
    U+0080 a U+07FF110xxxxx 10xxxxxx
    U+0800 a U+FFFF1110xxxx 10xxxxxx 10xxxxxx
    U+10000 a U+10FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

    Note que:

    Exemplos:
    1. O símbolo © tem código UNICODE U+00A9.
      Em binário A9 é 1010 1001. Usando a codificação de 2 bytes para a faixa U+0080 a U+07FF temos:

      11000010 10101001 = 0xC2 0xA9

      O primeiro byte começa com 110, indicando que a sequência é composta por dois bytes. A seguir vêm os cinco primeiros bits do código UNICODE (note o preenchimento com zeros à esquerda para completar a porção do código do caractere colocada no primeiro byte da sequência).

      O segundo byte começa com 10, indicando que é um byte de continuação. A seguir vêm os próximos seis bits do código UNICODE.

    2. O símbolo ≠ tem código UNICODE U+2260.
      Em binário 2260 é 0010 0010 0110 0000. Usando a codificação de 3 bytes para a faixa U+0800 a U+FFFF temos:

      11100010 10001001 10100000 = 0xE2 0x89 0xA0

      O primeiro byte começa com 1110, indicando que a sequência é composta por três bytes. A seguir vêm os quatro primeiros bits do código UNICODE

      O segundo e o terceiro bytes começam com 10, indicando que são bytes de continuação. Cada um deles tem, em seguida, os próximos seis bits do código UNICODE.

    Codificação UTF-16

    Na codificação UTF-16, os códigos dos caracteres são representados em um ou dois inteiros de 16 bits (chamados code units).

    Códigos UNICODE na faixa U+0000 a U+FFFF são representados em UTF-16 pelo seu próprio valor numérico (em 16 bits).

    Exemplos:

    Códigos UNICODE na faixa U+10000 a U+10FFFF são codificados em um par de code units, sendo cada elemento desse par um valor inteiro de 16 bits no intervalo [0xD800, 0xDFFF].

    A codificação UTF-16 leva em consideração o fato de que não há caracteres UNICODE válidos com código no intervalo [0xD800, 0xDFFF], por isso não há risco de confundir os elementos de um par de code units com um código UNICODE válido da faixa U+0000 a U+FFFF.

    Para encontrar o par de code units para um caractere UNICODE na faixa U+10000 a U+10FFFF é executado o seguinte algoritmo:

    1. Subtrai-se 0x10000 do valor numérico do código UNICODE. O resultado é um valor no intervalo [0, 0xFFFFF]. Desse resultado, consideramos apenas os 20 bits menos significativos (os outros 12 bits sempre têm o valor 0). Com esses 20 bits compomos o par de code units.
    2. Os 10 primeiros bits (mais significativos) são somados com 0xD800: o resultado é o primeiro code unit (um valor no intervalo [0xD800, 0xDBFF]).
    3. Os próximos 10 bits (menos significativos) são somados com 0xDC00. O resultado é o segundo code unit (um valor no intervalo [0xDC00, 0xDFFF]).

    Podemos também ver a composição dos dois elementos do code unit a partir do resultado da subtração do passo 1 da seguinte forma:

    Primeiro code unit1101 10xx xxxx xxxx
    Segundo code unit1101 11xx xxxx xxxx

    onde os "x" do primeiro code unit são os primeiros 10 bits do resultado da subtração, e os "x" do segundo code unit são os próximos 10 bits do resultado da subtração.

    Exemplo:

    Byte Order Mark (BOM)

    O caractere BOM (código U+FEFF) é um caractere especial opcionalmente inserido no início de um arquivo que contenha texto codificado em UNICODE.

    O BOM funciona como uma assinatura do arquivo: além de identificar o conteúdo do arquivo como UNICODE, ele também define a ordem de armazenamento do arquivo (big-endian ou little-endian). Como a ordem de armazenamento não faz sentido para arquivos UTF-8, o BOM é raramente usado no início de arquivos UTF-8, e seu uso não é recomendado pelo padrão. Além disso, muitas aplicações que trabalham com UTF-8 não dão suporte a um BOM no inicio do arquivo.

    A sequência exata de bytes que corresponde ao caractere BOM inserido no início do arquivo depende da codificação empregada e da ordem de armazenamento do arquivo. Para a codificação UTF-16, temos:

    Tipo do Arquivo Bytes
    UTF-16, big-endianFE FF
    UTF-16, little-endianFF FE


    Funções de Conversão

    O objetivo deste trabalho é implementar, na linguagem C, as funções utf8_16 e utf16_8, que realizam a conversão de um formato UNICODE (UTF-8 ou UTF-16) para o outro formato.

    Conversão UTF-8 para UTF-16

    A função utf8_16 deve ler o conteúdo de um arquivo de entrada (um texto codificado em UTF-8) e gravar em um arquivo de saída esse mesmo texto, codificado em UTF-16, com ordenação BIG-ENDIAN .

    O arquivo de entrada UTF-8 não terá um caracter BOM inicial; já o arquivo de saída, UTF-16, deverá necessariamente conter o BOM inicial.

    O protótipo (cabeçalho) da função utf8_16 é o seguinte:

      int utf8_16(FILE *arq_entrada, FILE *arq_saida);
    
    Os dois parâmetros da função são dois arquivos abertos em modo binário: o arquivo de entrada (arq_entrada) e o arquivo de saída (arq_saida).

    O valor de retorno da função utf8_16 é 0, em caso de sucesso e -1, em caso de erro de E/S. Em caso de erro, a função deve emitir, na saída de erro (stderr), uma mensagem indicando qual o tipo de erro ocorrido (leitura ou gravação) e retornar imediatamente.

    Por simplicidade, você pode considerar que os arquivos de entrada sempre conterão um texto CORRETAMENTE CODIFICADO. Dessa forma, você não precisa implementar o tratamento de erros de codificação do arquivo de entrada.


    Conversão UTF-16 para UTF-8

    A função utf16_8 deve ler o conteúdo de um arquivo de entrada (um texto codificado em UTF-16 com ordenação BIG-ENDIAN) e gravar em um arquivo de saída esse mesmo texto, codificado em UTF-8.

    O arquivo de entrada UTF-16 terá um BOM inicial, mas o arquivo de saída UTF-8 não deverá conter um BOM inicial.

    O protótipo da função é o seguinte:

      int utf16_8(FILE *arq_entrada, FILE *arq_saida);
    
    Os parâmetros da função são dois arquivos abertos em modo binário: o arquivo de entrada (arq_entrada) e o arquivo de saída (arq_saida).

    A função deve inspecionar os primeiros dois bytes do conteúdo do arquivo de entrada (UTF-16) para verificar se eles contêm um BOM válido!

    Assim como na função anterior, o valor de retorno é 0, em caso de sucesso e -1, em caso de erro. Os procedimentos para os casos de erro são:

    Nos dois casos, a função deve retornar imediatamente após emitir a mensagem de erro.

    Por simplicidade, você pode considerar que os arquivos de entrada sempre conterão um texto CORRETAMENTE CODIFICADO. Apenas o caso de BOM inválido (ou ausente) no início do arquivo precisa ser tratado.


    Implementação e Execução

    Você deve criar um arquivo fonte chamado conv_utf.c contendo as duas funções descritas acima, e funções auxiliares, se for o caso. Esse arquivo não deve conter uma função main!

    O arquivo conv_utf.c deverá incluir o arquivo de cabeçalho conv_utf.h , fornecido aqui.

    Para testar seu programa, crie um outro arquivo, por exemplo, teste.c, contendo uma função main. Crie seu programa executável, por exemplo teste, com a linha:

    gcc -Wall -o teste conv_utf.c teste.c
    

    Tanto o arquivo conv_utf.c como teste.c devem conter a linha:

    #include "conv_utf.h"
    

    Exemplos para teste

    Para testar suas funções você pode usar os arquivos fornecidos a seguir:

    Dicas

    Implemente seu trabalho por partes, testando cada parte implementada antes de prosseguir.

    Por exemplo, você pode implementar primeiro a leitura de um arquivo UTF-8, decodificando os caracteres para códigos UNICODE de 32 bits e exibindo esses valores na tela. Quando essa parte estiver funcionando, implemente a codificação UTF-16 sobre os valores, gerando um arquivo de saída.

    Para verificar o conteúdo do arquivo gravado, você pode usar o utilitário hexdump. Por exemplo, o comando

    hexdump -C <nome-do-arquivo>
    
    exibe o conteúdo do arquivo especificado byte a byte, em hexadecimal (16 bytes por linha). A segunda coluna de cada linha (entre '|') exibe os caracteres ASCII correspondentes a esses bytes, se eles existirem. Experimente inspecionar os arquivos dados como exemplo com o hexdump...

    Para abrir um arquivo para gravação ou leitura em formato binário, use a função

    FILE *fopen(char *path, char *mode);
    
    descrita em stdio.h. Seus argumentos são: A letra 'b', que indica o modo binário, é ignorada em sistemas como Linux, que tratam da mesma forma arquivos de tipos texto e binário. Mas ela é necessária em outros sistemas, como Windows, que tratam de forma diferente arquivos de tipos texto e binário (interpretando/modificando, por exemplo, bytes de arquivos "texto" que correspondem a caracteres de controle).

    Para fazer a leitura e gravação do arquivo, uma sugestão é pesquisar as funções fwrite/fread e fputc/fgetc.


    Entrega

    Deverão ser entregues via Moodle dois arquivos:

    1. Um arquivo fonte chamado conv_utf.c , contendo as funções utf8_16 e utf16_8 (e funções auxiliares, se for o caso).
      • Esse arquivo não deve conter a função main, e só deve incluir o arquivo de cabeçalho conv_utf.h e arquivos de cabeçalho da biblioteca padrão de C.
      • Coloque no início do arquivo, como comentário, os nomes dos integrantes do grupo da seguinte forma:
        /* Nome_do_Aluno1 Matricula Turma */
        /* Nome_do_Aluno2 Matricula Turma */
        
    2. Um arquivo texto, chamado relatorio.txt, contendo um pequeno relatório.
      • O relatório deverá explicar o que está funcionando e o que não está funcionando. Não é necessário documentar suas funções no relatório. Seu código deverá ser claro o suficiente para que isso não seja necessário.
      • Coloque também no relatório o nome dos integrantes do grupo
    Indique na área de texto da tarefa do Moodle o nome dos integrantes do grupo. Apenas uma entrega deve ser feita se os dois integrantes pertencerem à mesma turma.