code.pablomurad.com

skull-browser

Keyboard-driven browser for the small web, with native Gopher and Gemini

  1. 2d7abf9
    chore: drop a stray .deb that got committed, and ignore them
    

    Pablo Murad

  2. 0f036e4
    feat: own visual identity, and gemini over a shared small web layer
    
    F7, the visual identity. The shared chrome stylesheet is now a set of
    CSS custom properties on :root, with a light variant behind
    prefers-color-scheme, and the nine internal pages draw their colours
    from those tokens instead of hardcoding them. The GTK theme in
    theme.lua is rebuilt around the same phosphor palette, private tabs
    read as their own mode rather than a darker shade, and the new tab page
    is an authored plate with the mark, the name and the keys worth
    knowing, its colours taken from the theme.
    
    F8, the small web. gopher and gemini now share lib/smallweb: one page
    shell, one stylesheet, and a render mode setting that applies to both.
    Terminal mode is full width and monospaced with a caret on every link;
    page mode reads like a document. gopher was refactored onto it, so the
    two schemes cannot drift.
    
    gemini:// is new. It speaks the protocol directly over TLS through
    LuaSec, renders gemtext to HTML, follows input and redirect status
    codes, and escapes every field so a hostile page cannot become markup.
    Trust is trust on first use, in lib/smallweb/tofu: the first
    certificate a host presents is remembered and a change is reported, not
    accepted, with the choice to trust the new one. Without LuaSec the
    scheme still loads and every page says what to install.
    
    skull://smallweb (gS) switches the render mode and lists the remembered
    gemini hosts, forgetting any of them. Verified end to end against
    geminiprotocol.net: the handshake completes and the fingerprint is
    stored.
    
    Tests cover gemtext rendering including markup escaping, url parsing
    and link resolution, and the full tofu lifecycle. 97 passing.
    

    Pablo Murad

  3. 15fab09
    docs: everything in English, and a security model in the README
    
    The comments written during this fork were in Portuguese, in ten files
    across Lua, C and shell. They are English now, and so is every message
    setup-deps.sh prints. docs/decisoes is docs/decisions, and the decision
    record about staying on webkit2gtk-4.1 is translated with it.
    
    The README gains three sections.
    
    Security model states what is on and off by default, and then says the
    part that matters: there is no sandbox around Lua. A module under
    ~/.config/skull runs with everything the user account has, and so does
    a userscript installed with :usinstall. Saved form data is plain Lua
    the browser executes on every page load, 0600 and host-anchored but not
    encrypted, and it says so rather than implying otherwise. Certificate
    exceptions, what they cover and how to see and revoke them.
    
    Extending covers the tree layout, writing a module, adding a
    skull:// page in ten lines, and adding binds and commands.
    
    Contributing covers the build, the suite, the style checks that fail
    the build, where tests live and what a commit message looks like.
    
    Two claims in the README were wrong and are corrected: there is no `?`
    binding for help, only :help, and run-tests takes no arguments.
    

    Pablo Murad

  4. 8fc4eac
    feat: certificate exceptions expire, and can be seen and revoked
    
    Two of the certificate error strings were wrong, and they were the two
    that matter. A revoked certificate had no entry at all, so it showed as
    "Unknown error code revoked", while "insecure", which means the
    certificate was signed with a weak algorithm, was labelled "The
    certificate has been revoked."
    
    An exception, meaning a decision to trust one certificate that failed
    verification, used to last forever. It now lapses after 90 days, set by
    error_page.cert_exception_lifetime, and expired rows are swept when the
    store is read and before one is reapplied. The button says how long it
    will last instead of "permanently".
    
    skull://certs lists every stored exception with the date it was made
    and the date it lapses, and revokes any of them. Bound to gC and to
    :certs. WebKit keeps a certificate it has already been told to allow
    until the browser restarts, and the page says so rather than implying
    the revoke takes effect at once.
    
    The status bar showed (trust) for a host with an exception, because
    WebKit reports no certificate errors once the exception is in place. It
    shows (exception) in the untrusted colour now.
    
    Correcting an earlier note of mine: the stored certificate itself was
    never the problem. luakit.allow_certificate pins one certificate to one
    host through webkit_web_context_allow_tls_certificate_for_host, so a
    different certificate on that host was already refused. What was
    missing was the expiry, the listing and the indicator.
    

    Pablo Murad

  5. 6dd826d
    fix: webview settings were applied after the load had started
    
    Everything but the user agent waited for load-status "committed", which
    fires once the document is already being parsed. A new view therefore
    ran on WebKit's own defaults until its first page was on the way in,
    and a per-domain enable_javascript = false was a race against the
    parser rather than a setting.
    
    Global defaults are now applied in the constructor, and domain rules at
    "provisional", when the target uri is known and nothing has been
    parsed yet.
    
    get_setting_for_view could not be called before a load because it
    passed a nil uri to domains_from_uri, which raises. A view with no uri
    now resolves to the global defaults, which is the only thing that can
    apply to it.
    

    Pablo Murad

  6. 897687f
    fix: data hygiene and network posture
    
    umask is now 0077 for the whole process. Only the cookie jar was
    narrowed before, so history, bookmarks, downloads, sessions, the saved
    forms and the whole cache inherited whatever the login shell had set.
    Every file the browser writes is 0600 now, verified by running it
    against a throwaway data directory.
    
    Downloads started in a private tab no longer reach the database.
    history.lua has always made that check on every visit; downloads did
    not, so the uri and the path outlived the tab that asked for them. The
    view was already being passed to downloads.add and ignored.
    
    A typed address without a scheme now goes to https. It used to go to
    http, so the first request for any address the user typed went out in
    the clear, and a redirect to https could be stripped on the way.
    
    Referer is cut back to the origin within a domain, and dropped entirely
    when a secure page links to an insecure one. Cross-domain was already
    dropped. Paths and queries carry search terms, document names and
    tokens that the server on the other end has already seen once.
    
    An unparseable proxy uri now raises instead of quietly setting nothing.
    webkit_network_proxy_settings_new returns NULL for a uri it cannot
    read, and that NULL was passed straight on: traffic left direct while
    soup.proxy_uri still named the proxy. The stored value is only updated
    once the setting took.
    
    The mime decision handler logged every requested uri at info level,
    private tabs included, to stderr and to skull://log. It is verbose now.
    
    The formfiller chmod ran through os.execute, a real shell, with Lua's
    %q for quoting, which does not cover $ or the backtick. It uses
    luakit.spawn and shell_escape now, though with the new umask the file
    is created 0600 to begin with.
    
    enable_java, enable_plugins and enable_xss_auditor are still accepted
    by WebKitGTK and still settable, but WebKit no longer acts on any of
    them. They are marked as such where they are documented, rather than
    removed, so existing configs keep loading. Correcting an earlier note
    of mine: enable_hyperlink_auditing is not among them, it still works.
    

    Pablo Murad

  7. 6cc0495
    fix: escape page data in internal pages, and drop the shell from styles
    
    The bookmarks, history, downloads and log pages built their HTML by
    interpolating values straight into a template. Titles were escaped;
    uris, tags, dates, domains, ids and log messages were not. All of those
    come from the page that was visited, and all four pages export Lua
    functions to their own origin, so markup that lands there runs with the
    bridge in reach.
    
    The settings page wrote the error text back with innerHTML. It is
    textContent now, which cannot carry markup at all.
    
    styles.lua ran its watcher through `bash -c`, interpolating the path
    with Lua's %q. That escapes the double quote and the backslash but not
    the single quote, and here a real shell was on the other end, so a
    stylesheet whose name held one ran the rest of the name as a command.
    The watcher now calls inotifywait directly, with no shell, and pauses a
    second before retrying when the tool is missing.
    
    The quoting lives in lousy.util.shell_escape next to sql_escape and
    lua_escape, since luakit.spawn parses its argument the way a shell would
    even when no shell runs. Its test feeds hostile paths through
    spawn_sync and checks each one comes back as a single unchanged
    argument.
    

    Pablo Murad

  8. 94bd047
    fix: IPC socket was open to every local process
    
    The socket was created in the shared temporary directory with the
    default mode, so any process on the machine could connect to it.
    Anything that connects can send IPC_TYPE_page_created carrying a page
    id of its choosing, which rebinds a real tab's endpoint to its own
    socket.
    
    It now lives in a per-user runtime directory created with mode 0700,
    whose owner and mode are verified when it already exists. The socket
    itself is set to 0600 between bind() and listen(), where nothing can
    connect yet, so there is no window. Peers are checked with SO_PEERCRED
    on accept() and refused when they belong to another user.
    
    The directory is named skull-ipc rather than skull because glib falls
    back to the cache directory when XDG_RUNTIME_DIR is unset, and the
    browser already keeps its cache in skull/ there.
    

    Pablo Murad

  9. 61ad9ce
    fix: download paths and formfiller credential handling
    
    viewpdf built its destination as dir .. filename with the name taken from
    Content-Disposition, so a name carrying ../ or a leading slash wrote
    wherever the user can write. Targets that turn that into execution are
    right there in the data directory: forms.lua runs on every page load,
    userscripts run on matching pages, newtab.html is served verbatim. Only
    the last path component is used now.
    
    downloads set allow_overwrite before deciding the destination, so any
    computed path clobbered an existing file with no prompt. It is now scoped
    to the branch where a handler or the user named the file.
    
    formfiller matched rules with an unanchored string.find against an escaped
    full URI, so a rule for bank.example/login also matched
    evil.example/x#bank.example/login. Autofill guarded against this by
    checking the page host; zl and zL did not, and a rule may carry submit, so
    one keypress on a hostile page could fill a password and send it. The host
    check now covers all three paths.
    
    forms.lua holds passwords in cleartext and was created with default
    permissions. It gets 0600 on creation, the same treatment the cookie jar
    already had.
    
    to_lua_str escaped quote and backslash but not newline. A form attribute
    containing one produced a broken chunk appended to the credential file,
    which then failed to load entirely.
    
    This lowers the risk; it does not make the formfiller safe. The file is
    still cleartext Lua executed on every page load.
    
    Suite: 78 of 79, the failure being test_scroll timing out under a shared
    display. CI runs it on its own Xvfb and passes.
    

    Pablo Murad

  10. a2df913
    fix: escape error page fields at the boundary, not per caller
    
    show_error_page escaped nothing, and three callers handed it attacker data:
    the blocked URI in adblock_chrome, the requested path in help_chrome, and
    the page name in chrome.lua, which comes from ^skull://([^/]+) and so
    accepts <, > and quotes. Error pages force enable-scripts on, and two of
    those render on a skull:// origin where the Lua bridge lives.
    
    load_error_page now escapes every field the caller supplies except content,
    style and buttons, which carry markup by design. Future callers are covered
    without having to know.
    
    help_chrome needed two fixes of its own. It concatenated the path into
    content, which stays raw, so the path is now passed as a field. And
    lousy.load(prefix .. file) accepted .. and absolute paths, reading outside
    the documentation directory.
    
    Removed the two call site escapes added earlier; the boundary handles them
    now and doubling would render entities on screen. The gopher escape stays:
    it passes a socket error as content, which the boundary does not touch.
    
    Suite: 79 tests, all passing.
    

    Pablo Murad

  11. 5e5edc8
    fix: unpickle executed its input as Lua code
    
    lousy.pickle.unpickle was loadstring("return " .. s) followed by a call.
    Four callers feed it outside data: the single instance D-Bus channel
    (lib/unique_instance.lua:59), the session file (lib/session.lua:104), the
    command history (lib/cmdhist.lua:99) and the settings file
    (lib/settings.lua:30).
    
    The D-Bus one is remote in practice. GApplication exports org.gtk.Actions
    on the session bus, so any process that reaches it - including a flatpak
    started with --socket=session-bus - sends a string that becomes Lua running
    in the UI process, with os.execute and the cookie database in scope.
    
    Replaced with a reader that parses instead of executing. It accepts only
    what pickle() emits: table literals, string, number and boolean literals,
    and {n} table references. No call, no operator, no identifier other than
    true, false and nil is reachable. Nesting is capped and trailing data is
    refused.
    
    On-disk format is unchanged, so existing sessions, command history and
    settings still load without migration.
    
    Also renamed the application id from org.luakit to org.skullbrowser, which
    was squatting on a real luakit install's bus name.
    
    Tests cover both halves: the format round-trips strings holding quotes,
    backslashes, newlines, NUL and high bytes, shared table references survive,
    and seven code execution payloads are refused.
    
    Suite: 79 tests, all passing.
    

    Pablo Murad

  12. 6c47fac
    feat: adblock funcional, CI, about redesenhada, e remocao de API morta
    
    adblock_bootstrap baixa e mantem as listas de filtro. O modulo adblock
    lia .txt de um diretorio e nada mais: sem capacidade de buscar lista
    alguma, uma instalacao nova bloqueava exatamente nada. Agora baixa
    EasyList, EasyPrivacy e a lista em portugues via curl, valida o conteudo
    antes de aceitar (uma pagina de erro HTML seria parseada como regras sem
    reclamar) e recarrega. O modulo nao faz nada ao ser carregado: quem
    dispara e o rc.lua, o que mantem a suite de testes fora da rede.
    
    Corrigido no adblock.lua: load transmitia update_rules mas nao o estado
    de cada lista, entao processo web existente ficava com a lista carregada
    e desligada ate alguem alternar na mao.
    
    CI em GitHub Actions: dependencias, build e testes a cada push e PR.
    
    process_limit removido por inteiro. O codigo que aplicava o valor estava
    sob #if !WEBKIT_CHECK_VERSION(2,26,0), morto ha anos, enquanto a API
    seguia exposta ao Lua aceitando valores. O proprio rc.lua definia
    process_limit = 4, com um comentario acima admitindo que nao tinha
    efeito.
    
    docs/decisoes/001 registra por que ficamos no webkit2gtk-4.1 e a regra de
    nao crescer extension/.
    
    skull://about redesenhada em ingles: nome, versao, developed by pmurad,
    link para pablomurad.com, e a mencao ao luakit sob GPLv3.
    
    Titulo da janela volta a ser texto puro. O emoji so renderiza se o proprio
    browser desenhar a barra de titulo; com GTK_WINDOW_TOPLEVEL sem titlebar
    quem desenha e o compositor, que no WSLg roda em outro conteiner com as
    fontes dele.
    

    Pablo Murad

  13. 23c8ac8
    fix: injecao de HTML no gopher e laco infinito no error_page
    
    Um servidor gopher hostil executava script no browser sem nenhuma
    interacao do usuario. Confirmado, nao hipotetico.
    
    O vetor principal era o href do menu. href_source honrava um prefixo URL:
    no seletor com captura gulosa e zero sanitizacao, e menu_to_html emitia o
    resultado cru dentro de href="". Uma linha de menu como
    
      1Label<TAB>URL:x"><img src=x onerror="..."><TAB>host<TAB>70
    
    fechava o atributo e disparava o onerror na abertura da pagina. JavaScript
    esta ligado nessas paginas por padrao - o proprio gopher depende disso para
    a busca tipo 7.
    
    Vetor secundario: url.title ia cru para <title>. Vem do seletor ja passado
    por uri_decode, que materializa < e >. Exige navegacao induzida, mas o
    script cai na origem do host gopher nomeado, que o atacante nao precisa
    controlar.
    
    Correcoes em gopher.lua: escape no href e nos dois pontos que emitem
    <title>, lista de esquemas aceitos no prefixo URL: (javascript e data
    rejeitados) e sanitizacao do campo host, que vinha cru do servidor
    enquanto a porta ja era filtrada.
    
    Em error_page.lua, problema separado: a substituicao de {palavra} repetia
    ate nsub == 0, mas gsub com tabela conta a ocorrencia mesmo quando a chave
    nao existe e nada e substituido. Uma unica chave desconhecida travava a
    interface para sempre. Agora ha teto de iteracoes, e uri e err.message
    passam por escape - vem do WebKit e ninguem os tratava.
    
    Seis testes novos com entrada hostil. Eram os primeiros testes de escape
    do projeto: nao havia nenhum, nem para lousy.util.escape.
    
    Suite: 75 testes, todos passando.
    

    Pablo Murad

  14. 4967e70
    build: plugins de midia do GStreamer, e nota de uso sob WSL
    
    O WebKit reclamava em tempo de execucao de encoder de WebVTT e de
    fakevideosink ausentes: faltavam gst-plugins-bad e gst-libav, que o
    setup-deps nao instalava. Legendas e parte da reproducao de video ficavam
    degradadas. Nomes conferidos nas tres distros.
    
    README: chamar 'skull' pelo nome a partir do Windows exige shell de login
    (wsl -- bash -lc), senao ~/.local/bin nao entra no PATH e o comando nao
    resolve. Dentro do WSL funciona direto.
    

    Pablo Murad

  15. 54d6dd7
    feat: Google volta a ser o buscador padrao
    
    Praticidade no caminho comum. Os independentes continuam disponiveis por
    atalho: sx (SearXNG), marginalia, mojeek, wiby. DuckDuckGo volta como ddg.
    
    Pagina inicial segue skull://newtab/ - nao tem relacao com busca.
    
    Suite: 69 testes, todos passando.
    

    Pablo Murad

  16. 06ece15
    feat: pagina inicial propria e buscadores independentes por padrao
    
    window.home_page apontava para https://luakit.github.io/ - abrir a pagina
    inicial no Skull Browser levava ao site do projeto original. Agora e
    skull://newtab/, que carrega sem rede e nao avisa ninguem que o browser
    abriu.
    
    Buscadores: sairam Google, DuckDuckGo e IMDB. O padrao passa a ser
    SearXNG, que e metabusca e qualquer um pode hospedar. Junto vieram
    Marginalia e Wiby, que indexam a web pequena e textual - a mesma que
    gopher e Gemini habitam - e Mojeek, com rastreador proprio. Nenhum dos
    quatro revende resultado de Google ou Bing.
    
    Endpoints conferidos com requisicao real: Marginalia, Wiby e searx.be
    responderam 200. Mojeek devolveu 403 para curl por bloqueio de agente,
    o formato da URL esta correto mas nao foi possivel confirmar de fato.
    
    Instancia publica de SearXNG tem limite de requisicao; trocar para uma
    propria e uma linha na config do usuario.
    
    Suite: 69 testes, todos passando.
    

    Pablo Murad

  17. 1e313d3
    feat: comando vira skull, e buildopts.h passa a acompanhar o PREFIX
    
    O comando era skull-browser; agora e skull, e o rename foi ate o fim em
    vez de parar no executavel: .so, man page, share/, lib/, os diretorios
    XDG e de cache/config/data, o .desktop e os icones. O nome de exibicao
    continua Skull Browser, e o esquema skull:// agora casa com o binario.
    
    Correcao de build herdada do luakit: a regra do buildopts.h so disparava
    quando buildopts.h.in mudava. Como PREFIX e XDGPREFIX chegam pela linha
    de comando, compilar com outro prefixo mantinha os caminhos da primeira
    build cravados no binario, que ia procurar modulos no lugar errado. A
    regra agora regera sempre e so substitui o arquivo quando o conteudo
    muda, para nao recompilar a arvore a toa.
    
    README: a secao de configuracao mandava copiar rc.lua de
    /usr/local/etc/xdg, caminho que so existe em instalacao com root. Agora
    cobre os dois casos, inclusive o de instalacao no home, onde o arquivo
    cai fora dos diretorios que o browser procura.
    
    Suite: 69 testes, todos passando.
    

    Pablo Murad

  18. 6dc32de
    test: suite volta a rodar, e sem exigir root
    
    O rename do binario nao tinha alcancado o harness: run_test.lua ainda
    invocava ./luakit, entao todo teste morria em 'No such file'.
    
    Duas mudancas para rodar fora de um ambiente com root:
    
      SKULL_TEST_DISPLAY reaproveita um X server existente. O Xvfb quer
      /tmp/.X11-unix com dono proprio e um xkbcomp em /usr/bin; sob WSLg o
      diretorio e do sistema e o bind falha, mas ali ja existe display.
    
      LUA_PATH, LUA_CPATH e LD_LIBRARY_PATH passam a ser repassados ao
      processo filho. O runner usa env -i, que limpava tudo, e modulos Lua
      instalados fora do prefixo do sistema ficavam invisiveis.
    
    Dados de teste: eu tinha trocado as URIs para skull:// mas deixado o
    campo scheme esperando luakit em test_clib_soup e test_lib_lousy_uri.
    
    about_chrome ganhou o blurb de modulo e a documentacao do stylesheet que
    os testes de estilo exigem.
    
    setup-deps: xvfb e luacheck nas tres familias; removido lua-luassert do
    Fedora, que nao empacota isso - o aviso de luarocks agora cita luassert.
    
    Resultado: 69 testes, 68 passando. test_scroll oscila sob carga da suite
    completa em display compartilhado; isolado passa 4 de 4.
    

    Pablo Murad

  19. 5985430
    feat: pagina skull://about e README
    
    Pagina sobre com nome, versao e a mencao a base, acessivel por gA ou
    :about. O cranio vai embutido como SVG na propria pagina: nao existe
    handler de recurso para skull://icons, entao depender de arquivo externo
    quebraria.
    
    README reescrito em ingles, com instrucoes de build e instalacao por
    distro, incluindo o caminho sem root via PREFIX no home.
    
    luassert adicionado ao setup-deps: make run-tests falha sem ele.
    

    Pablo Murad

  20. 0beed7c
    feat: identidade propria - icones, binario skull-browser, esquema skull://
    
    Icones gerados por build-utils/gen-icons.js a partir de grades de pixel
    16x16: PNG RGBA sem antialiasing, que e o que mantem o desenho legivel no
    tamanho de aba. Seis glifos distintos (cranio para paginas internas,
    documento, cranio violeta para privado, cranio morto para crash, triangulo
    de alerta, cadeado aberto) mais o icone do aplicativo em PNG e SVG.
    
    Rename de instalacao, sem tocar no namespace Lua:
      binario, .so e man page -> skull-browser
      share/luakit, lib/luakit, XDG -> skull-browser
      diretorios de cache/config/data no C
      esquema das paginas internas luakit:// -> skull:// (26 arquivos)
    
    Correcoes encontradas no caminho:
      gopher: a regra CSS tinha virgula sobrando e era descartada inteira,
        entao a fonte monoespacada nunca valeu
      gopher: URI malformada levantava dentro do signal handler; agora vira
        pagina de erro
      gopher: erro de socket ia cru para o error_page, que expande {palavra}
        recursivamente e comia trechos da mensagem
      error_page: link de favicon apontava para skull://icons, que nao tem
        handler registrado
      webview.c: request de esquema sem web view retornava sem finish, e o
        carregamento ficava pendurado para sempre
      make install apontava para extras/luakit.desktop, ja renomeado
    
    Gopher deixa de vir comentado em rc.lua.
    
    Verificado no Ubuntu 24.04 / WebKitGTK 2.52.6: make limpo, binario roda,
    --check valida a config com o gopher carregado.
    

    Pablo Murad

  21. 60edfc7
    build: script de dependencias com suporte a apt, dnf e pacman
    
    Substitui o script so-Ubuntu. Detecta a familia da distro e instala os
    pacotes correspondentes; --check apenas verifica.
    
    Verificado no Ubuntu 24.04 (build completo). Fedora e Arch tem os nomes
    de pacote por conferir - a deteccao de distro e o relatorio funcionam,
    a instalacao em si nao foi testada nessas duas.
    

    Pablo Murad

  22. 0672a57
    build: script de preparacao do ambiente WSL/Ubuntu
    
    Instala e verifica as dependencias que o config.mk exige via pkg-config
    (gtk+-3.0, gthread-2.0, webkit2gtk-4.1, sqlite3, javascriptcoregtk-4.1,
    luajit) mais os modulos Lua de runtime (lfs, socket).
    
    Idempotente e verboso: falha com lista do que ficou faltando.
    Fase 0 do plano do 0.01.
    

    Pablo Murad

  23. f6a137f
    feat: identidade do fork - Skull Browser, autoria e titulo
    
    - AUTHORS: Pablo Murad <pablomurad@pm.me> como autor do fork, mantendo
      toda a autoria anterior do luakit conforme exige a GPLv3
    - lib/window.lua: titulo padrao da janela agora e 'Skull Browser'
    - extras/: luakit.desktop -> skull-browser.desktop (Name, Exec, Icon)
    

    Pablo Murad

  24. cc12505
    chore: remove upstream CI, changelog e docs de contribuicao do Luakit
    
    Removidos artefatos herdados que so faziam sentido no repositorio original:
    - .github/ (workflows de CI e template de issue apontando para luakit/luakit)
    - .travis.yml (CI desativado)
    - CHANGELOG.md e MIGRATE.md (historico de versoes do Luakit)
    - CONTRIBUTING.md (instrucoes de contribuicao do projeto original)
    

    Pablo Murad

  25. 3ea493c
    chore: initial import of skull browser
    
    Base de código derivada do Luakit (https://github.com/luakit/luakit),
    licenciado sob GNU GPLv3. Histórico upstream removido; este repositório
    inicia uma linha de desenvolvimento independente.
    

    Pablo Murad