O tar é o formato mais tradicional de arquivamento do universo Unix, sendo usado desde o final da década de 70, muito antes do ZIP sequer ser pensado.

O tar é diferente de outros formatos de arquivamento porque ele não comprime arquivos individualmente. Todos o arquivo tar é comprimido junto. Para extrair os arquivos, você deve primeiro descomprimir o arquivo .tar inteiro.

Para identificar o formato de compressão, você adiciona um sufixo no arquivo. Os formatos e sufixos suportados são:

SufixoFormato
‘.gz’gzip
‘.tgz’gzip
‘.taz’gzip
‘.Z’compress
‘.taZ’compress
‘.bz2’bzip2
‘.tz2’bzip2
‘.tbz2’bzip2
‘.tbz’bzip2
‘.lz’lzip
‘.lzma’lzma
‘.tlz’lzma
‘.lzo’lzop
‘.xz’xz
‘.zst’zstd
‘.tzst’zstd

O sufixo mais visto e o mais suportado é o .gz. Por isso que você vê muito .tar.gz.

O formato

O lado bom do arquivo .tar é que não existem valores binários: os valores binários dele estão nos arquivos, não nos metadados.

Todos os números são salvos em octal. Por exemplo, se você ver um 10 no arquivo, significa que o valor que está ali é 8, pois 10 em octal é 8 em decimal. Assim como 11=9, 12=10, 13=11, 17=15 e 20=16.

As strings têm um tamanho fixo. Se o tamanho delas no arquivo for menor que o tamanho fixo, vários bytes 0 são adicionados até chegarem nesse tamanho.

O arquivo tar é dividido em blocos. Cada bloco possui 512 bytes. Esse também é o tamanho do header contendo os metadados dos arquivos.

O arquivo já começa com o header do primeiro arquivo:

00000000  67 72 61 70 68 69 63 61  6c 73 62 6f 75 6e 64 69  |graphicalsboundi|
00000010  6e 67 2e 72 73 00 00 00  00 00 00 00 00 00 00 00  |ng.rs...........|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000060  00 00 00 00 30 30 30 30  37 37 37 00 30 30 30 31  |....0000777.0001|
00000070  37 35 30 00 30 30 30 31  37 35 30 00 30 30 30 30  |750.0001750.0000|
00000080  30 30 30 37 36 30 33 00  31 34 32 37 33 30 30 32  |0007603.14273002|
00000090  32 37 35 00 30 31 35 37  37 34 00 20 30 00 00 00  |275.015774. 0...|
000000a0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000100  00 75 73 74 61 72 20 20  00 61 72 74 68 75 72 6d  |.ustar  .arthurm|
00000110  63 6f 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |co..............|
00000120  00 00 00 00 00 00 00 00  00 61 72 74 68 75 72 6d  |.........arthurm|
00000130  63 6f 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |co..............|
00000140  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000001f0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
PosiçãoTamanhoCampoValor
0x0 (0)100namegraphicalsbounding.rs
0x64 (100)8mode0000777
0x6c (108)8uid0001750
0x74 (116)8gid0001750
0x7c (124)12size00000007603
0x88 (136)12mtime14273002275
0x94 (148)8chksum015774
0x9c (156)1typeflag30 = REGTYPE
0x9d (157)100linkname
0x101 (257)6magicustar
0x107 (263)2version0x20 0x00
0x109 (265)32unamearthurmco
0x129 (297)32gnamearthurmco
0x149 (329)8devmajor
0x151 (337)8devminor
0x159 (345)155prefix

Como eu falei, todos os números estão em formato octal.

Depois desse header, no próximo bloco está o conteúdo do arquivo.

Quando o arquivo termina, o header começa no próximo bloco, no próximo byte múltiplo de 512.

Ou seja, se o arquivo acabar no byte 5090, o header só vai começar no byte 5120, já que é o próximo múltiplo de 512.

Para sinalizar o final do arquivo, dois blocos vazios (ou seja, cheios de bytes 0) são escritos.

O arquivo tar é identificado pelo valor no campo magic, ou seja ustar. Existem mais tipos de arquivos tar, e eles serão discutidos nas próximas partes.

O checksum (escrito no campo chksum) é calculado somando todos os bytes do header. Os bytes que estariam no checksum são substituídos, na função do cálculo, por espaços ( , byte 32). O resultado é escrito no campo (no caso da escrita) ou comparado com o valor do campo (no caso da leitura)

uname e gname são os nomes do usuário e do grupo que criaram o arquivo. uid e gid são os IDs de usuário e grupo.

mtime é a data da última modificação do arquivo, ou de criação se o arquivo nunca foi alterado. O valor é a quantidade de segundos desde 01/01/1970.

name é o nome do arquivo. Se ele é um link que aponta pra outro arquivo, o nome desse arquivo vai estar em linkname.

devmajor e devminor só fazem sentido se o arquivo for um device, tipo aqueles arquivos que estão dentro da pasta /dev nos linuxes e unixes da vida.

Códigos

Inicialmente, definiremos os valores possíveis nos campos mode e typeflag:


from enum import Enum, IntFlag

# Isso são flags.
# Significa que mais de um valor pode ser possível aqui.
class FileMode(IntFlag):
    # O modo do arquivo
    # Quando você dá `ls -l` no terminal, tem um monte de rwxrwxrwx. 
    # Isso é codificado aqui.'
    # O significado está claro se você manja um pouco de inglês. 
    # Se não, separa as duas palavras e joga no google tradutor que vai estar 
    # correto.

    OtherExec = 1
    OtherWrite = 2
    OtherRead = 4

    GroupExec = 8
    GroupWrite = 16
    GroupRead = 32

    OwnerExec = 64
    OwnerWrite = 128
    OwnerRead = 256

    SetGid = 1024
    SetUid = 2048

class FileType(Enum):
    ## O tipo de arquivo

    # Um arquivo comum
    Regular = 0            

    # Um link pra outro arquivo
    Link = 1

    # Um link simbólico. Esse valor está depreciado e não é usado
    Symlink = 2

    # Se você sabe o que são essas duas coisas abaixo, então você sabe o que 
    # significa tudo aqui nesse enum, e não está nem lendo esses comentários.
    CharacterDevice = 3
    BlockDevice = 4

    # Representa uma pasta. Uma coisa engraçada é que, dentro do arquivo tar, 
    # logo depois da pasta,  vêm os arquivos que estão dentro dela. 
    # Isso pode ser útil pra você.
    Directory = 5

    # Se você sabe o que é isso daqui, então você nem precisa desses comentários
    FIFOPipe = 6

    # Campo reservado.
    Reserved = 7

    # Como converter do valor do arquivo pro valor do enum
    # O `val` é o caractere que vem do arquivo.
    @staticmethod
    def from_value(val):
        selections = {
            "0": FileType.Regular,
            "\0": FileType.Regular,
            "1": FileType.Link,
            "2": FileType.Symlink,
            "3": FileType.CharacterDevice,
            "4": FileType.BlockDevice,
            "5": FileType.Directory,
            "6": FileType.FIFOPipe,
            "7": FileType.Reserved
        }

        return selections.get(val, FileType.Reserved)

Algumas funções auxiliares, que vão ajudar a ler o arquivo


# Isso aqui embaixo vem no final:

# Lê uma string do arquivo
def read_string(value):
    return value.decode('utf-8').rstrip('\x00 ')

# Lê um número do arquivo. Ele está em octal, então devemos converter.
def read_number(value, default=None):
    try:
        return int(read_string(value), base=8)
    except ValueError:
        return default

Depois, vamos ler o arquivo TAR:


## Coloque isso no começo do arquivo, antes do `from enum`
from dataclasses import dataclass

## Isso aqui embaixo vem no final:

# O tamanho do bloco:
BLOCK_SIZE = 512

@dataclass
class TarFile:
    # Você já conhece esses campos :)
    name: str
    mode: FileMode
    uid: int
    gid: int
    size: int
    mtime: dt.datetime
    checksum: int
    typeflag: FileType
    linkname: str
    magic: str
    version: int
    uname: str
    gname: str
    devmajor: int
    devminor: int
    prefix: str

    # O offset do arquivo que segue esse header, pra gente poder ler ele
    # depois
    offset: int

    # O offset do próprio header.
    header: int

    def read_file(self, fileobject):
        # Lê o conteúdo do arquivo que esse header representa
        tell = fileobject.tell()

        fileobject.seek(self.offset)
        data = fileobject.read(self.size)
        fileobject.seek(tell)

        return data

    def get_header_offset(self):
        return self.offset - BLOCK_SIZE

    def verify_checksum(self, fileobject):
        # Verifica o checksum desse header, pra ver se ele é válido ou não.
        tell = fileobject.tell()

        fileobject.seek(self.get_header_offset())
        data = fileobject.read(BLOCK_SIZE)

        checksum = sum([v if i not in range(148, 156) else ord(' ')
                        for i, v in enumerate(data)])

        fileobject.seek(tell)

        return checksum == self.checksum

    @staticmethod
    def from_file(fileobject):
        # Lê um arquivo.
        #
        # Esse `fileobject` é um objeto de arquivo, gerado pelo método `open`, ou
        # por qualquer método que represente um arquivo, como `GzipFile` e outros
        # similares.
        #
        # Esse método vai alterar o fileobject, fazendo ele apontar pro próximo 
        # bloco, que na maior parte das vezes vai ser o conteúdo do arquivo.

        offset = fileobject.tell()

        name = read_string(fileobject.read(100))
        if name == "":
            return None

        mode = FileMode(read_number(fileobject.read(8)))
        uid = read_number(fileobject.read(8))
        gid = read_number(fileobject.read(8))
        size = read_number(fileobject.read(12))
        mtime = dt.datetime.fromtimestamp(read_number(fileobject.read(12)))
        checksum = read_number(fileobject.read(8))
        typeflag = FileType.from_value(read_string(fileobject.read(1)))
        linkname = read_string(fileobject.read(100))

        magic = read_string(fileobject.read(6))
        version = read_number(fileobject.read(2), 0)

        if not magic.startswith("ustar"):
            raise ValueError(f"Invalid header at offset {offset}")

        uname = read_string(fileobject.read(32))
        gname = read_string(fileobject.read(32))
        devmajor = read_number(fileobject.read(8))
        devminor = read_number(fileobject.read(8))
        prefix = fileobject.read(155)

        pad = fileobject.read(12)

        return TarFile(name, mode, uid, gid, size, mtime, checksum, typeflag, 
                       linkname, magic, version, uname, gname, devmajor, devminor,
                       prefix, offset + BLOCK_SIZE, offset)

Essa função acima só vai ler o header. Depois do header vem o arquivo.

E é isso. O necessário para ler o formato está descrito

Se você quiser, você pode transformar esse código que eu mostrei em um parser de arquivo .tar:

  • Você pode simplesmente usar open para ler um arquivo tar puro. O método TarFile.from_file aceita um objeto de arquivo que a função open retorna
  • Para ler um arquivo .tar.gz, use a classe GzipFile para ler o arquivo, já que esse é o formato que o tar está comprimido
  • Outros formatos têm outras bibliotecas de suporte. Se você quiser, pode suportá-las.
  • Lembre-se que dois blocos vazios, cheios de 0, identificam o final do arquivo…
  • Converta esse código para a sua linguagem favorita

Referências: