Skip to content

archive/tar: add support for writing tar containing sparse files #13548

Open

Description

@grubernaut

I've created a Github Repo with all the needed steps for reproducing this on Ubuntu 12.04 using Go1.5.1. I've also verified that using Go1.5.2 still experiences this error.

Run vagrant create then vagrant provision from repository root.

vagrant create
vagrant provision

Expected Output:

$ vagrant provision
==> default: Running provisioner: shell...
    default: Running: inline script
==> default: stdin: is not a tty
==> default: go version go1.5.2 linux/amd64
==> default: Creating Sparse file
==> default: Proving file is truly sparse
==> default: 0 -rw-r--r-- 1 root root 512M Dec  9 15:26 sparse.img
==> default: Compressing in Go without sparse
==> default: Compressing in Go with sparse
==> default: FileInfo File Size: 536870912
==> default: Proving non-sparse in Go gained size on disk
==> default: 512M -rw-r--r-- 1 root root 512M Dec  9 15:26 non_sparse/sparse.img
==> default: Proving sparse in Go DID keep file size on disk
==> default: 0 -rw-r--r-- 1 root root 0 Dec  9 15:26 sparse/sparse.img
==> default: Compressing via tar w/ Sparse Flag set
==> default: Proving sparse via tar DID keep file size on disk
==> default: 0 -rw-r--r-- 1 root root 512M Dec  9 15:26 tar/sparse.img

Actual Output:

$ vagrant provision
==> default: Running provisioner: shell...
    default: Running: inline script
==> default: stdin: is not a tty
==> default: go version go1.5.2 linux/amd64
==> default: Creating Sparse file
==> default: Proving file is truly sparse
==> default: 0 -rw-r--r-- 1 root root 512M Dec  9 15:35 sparse.img
==> default: Compressing in Go without sparse
==> default: Compressing in Go with sparse
==> default: Proving non-sparse in Go gained size on disk
==> default: 513M -rw-r--r-- 1 root root 512M Dec  9 15:35 non_sparse/sparse.img
==> default: Proving sparse in Go DID NOT keep file size on disk
==> default: 512M -rw-r--r-- 1 root root 512M Dec  9 15:35 sparse/sparse.img
==> default: Compressing via tar w/ Sparse Flag set
==> default: Proving sparse via tar DID keep file size on disk
==> default: 0 -rw-r--r-- 1 root root 512M Dec  9 15:35 tar/sparse.img

The Vagrantfile supplied in the repository runs the following shell steps:

  • Installs Go
  • Creates a sparse file via truncate -s 512M sparse.img
  • Proves that the file is sparse via ls -lash sparse.img
  • Runs compress.go via go run compress.go
  • Untars the archives created by compress.go via tar -xf
  • Verifies that the extracted files did not maintain sparse files, both with and without the sparse type set in the tar file's header. ls -lash sparse.img
  • Uses GNU/Tar to compress the sparse file with the sparse flag set tar -Scf sparse.tar sparse.img
  • Extracts the archive created by GNU/Tar tar -xf sparse.tar
  • Proves that GNU/Tar maintained sparse files ls -lash sparse.img

This is somewhat related to #12594.

I could also be creating the archive incorrectly, and have tried a few different methods for creating the tar archive, each one however, did not keep the sparse files intact upon extraction of the archive. This also cannot be replicated in OSX as HGFS+ does not have a concept of sparse files, and instantly destroys any file sparseness, hence the need for running and testing the reproduction case in a vagrant vm.

Any thoughts or hints into this would be greatly appreciated, thanks!

Activity

  1. bradfitz commented on Dec 9, 2015

    @bradfitz
    Contributor

    /cc @dsnet who's been going crazy on the archive/tar package in the Go 1.6 tree ("master" branch)

  2. dsnet commented on Dec 9, 2015

    @dsnet
    Member

    This isn't a bug per-say, but more of a feature request. Sparse file support is only provided for tar.Reader, but not tar.Writer. Currently, it's a bit asymmetrical, but supporting sparse files on tar.Writer requires API change, which may take some time to think about.

    Also, this is mostly unrelated to #12594. Although, that bug should definitely be fixed before any attempt at this is made. For the time being, I recommend putting this in the "unplanned" milestone, I'll revisit this issue when the other tar bugs are first fixed.

  3. grubernaut commented on Dec 9, 2015

    @grubernaut
    Author

    @dsnet should I keep this here as a feature request, or is there another preferred format for those?

  4. dsnet commented on Dec 9, 2015

    @dsnet
    Member

    The issue tracker is perfect for that. So this is just fine.

  5. changed the title [-]archive/tar: Writing a tarfile does not maintain sparse files[/-] [+]archive/tar: no support for writing tar containing sparse files[/+] on Dec 28, 2015
  6. added this to the Unplanned milestone on Dec 28, 2015
  7. changed the title [-]archive/tar: no support for writing tar containing sparse files[/-] [+]archive/tar: add support for writing tar containing sparse files[/+] on Dec 28, 2015
  8. dsnet commented on Feb 26, 2016

    @dsnet
    Member

    This my proposed addition to the tar API to support sparse writing.

    First, we modify tar.Header to have an extra field:

    type Header struct {
        ...
    
        // SparseHoles represents a sequence of holes in a sparse file.
        //
        // The regions must be sorted in ascending order, not overlap with
        // each other, and not extend past the specified Size.
        // If len(SparseHoles) > 0 or Typeflag is TypeGNUSparse, then the file is
        // sparse. It is optional for Typeflag to be set to TypeGNUSparse.
        SparseHoles  []SparseHole
    }
    
    // SparseEntry represents a Length-sized fragment at Offset in the file.
    type SparseEntry struct {
        Offset int64
        Length int64
    }

    On the reader side, nothing much changes. We already support sparse files. All that's being done is that we're now exporting information about the sparse file through the SparseHoles field.

    On the writer side, the user must set the SparseHoles field if they intend to write a sparse file. It is optional for them to set Typeflag to TypeGNUSparse (there are multiple formats to represent sparse files so this is not important). The user then proceeds to write all the data for the file. For sparse holes, they will be required to write Length zeros for that given hole. It is a little inefficient writing zeros for the holes, but I decided on this approach because:

    • It is symmetrical with how tar.Reader already operates (which transparently expands a sparse file).
    • It is more representative of what the "end result" really looks like. For example, it allows a user to write a sparse file by just doing io.Copy(tarFile, sparseFile) and not worry about where the holes are (assuming they already populated the SparseHoles field).

    I should note that the tar format represents sparse files by indicating which regions have data, and treating everything else as a hole. The API exposed here does the opposite; it represents sparse files by indicating which regions are holes, and treating everything else as data. The reason for this inversion is because it fits the Go philosophy that the zero value of some be meaningful. The zero value of SparseHoles indicates that there are no holes in the file, and thus it is a normal file; i.e., the default makes sense. If we were to use SparseDatas instead, the zero value of that indicates that there is no data in the file, which is rather odd.

    It is a little inefficient requiring that users write zeros and the bottleneck will be the memory bandwidth's ability to transfer potentially large chunks of zeros. Though not necessary, the following methods may be worth adding as well:

    // Discard skips the next n bytes, returning the number of bytes discarded.
    // This is useful when dealing with sparse files to efficiently skip holes.
    func (tr *Reader) Discard(n int64) (int64, error) {}
    
    // FillZeros writes the next n bytes by filling it in with zeros.
    // It returns the number of bytes written, and an error if any.
    // This is useful when dealing with sparse files to efficiently skip holes.
    func (tw *Writer) FillZeros(n int64) (int64, error) {}

    Potential example usage: https://play.golang.org/p/Vy63LrOToO

  9. ianlancetaylor commented on Feb 26, 2016

    @ianlancetaylor
    Contributor

    If Reader and Writer support sparse files transparently, why export SparseHoles? Is the issue that when writing you don't want to introduce a sparse hole that the caller did not explicitly request?

  10. dsnet commented on Feb 26, 2016

    @dsnet
    Member

    The Reader expands sparse files transparently. The Writer is "transparent" in the sense that a user can just do io.Copy(tw, sparseFile) and so long as the user already specified where there sparse holes are, it will avoid writing the long runs of zeros.

    Purely transparent sparse files for Writer cannot easily done since the tar.Header is written before the file data. Thus, the Writer cannot know what sparse map to encode in the header prior to seeing the data itself. Thus, Writer.WriteHeader needs to be told where the sparse holes are.

    I don't think tar should automatically create sparse files (for backwards compatibility). As a data point, the tar utilities do not automatically generate sparse files unless the -S flag is passed in. However, it would be nice if the user didn't need to come up with the SparseHoles themselves. Unfortunately, I don't see an easy solution to this.


    There are three main ways that sparse files may be written:

    1. In the case of writing a file from the filesystem (the use case that spawned this issue is of this), I'm not aware of any platform independent way to easily query for all the sparse holes. There is a method to do this on Linux and Solaris with SEEK_DATA and SEEK_HOLE (see my test in CL/17692), but I'm not aware of ways to do this on other OSes like Windows or Darwin.
    2. In the case of a round-trip read-write, a tar.Header read from Reader.Next and written to Writer.WriteHeader will work just fine as expected since tar.Header will have the SparseHoles field populated.
    3. In the case of writing a file from a memory, the user will need to write their own zero detection scheme (assuming they don't already know where the holes are).

    I looked at the source for GNU and BSD tar to see what they do:

    • (Source) BSD tar attempts to use FIEMAP first, then SEEK_DATA/SEEK_HOLE, then (it seems) it avoids sparse files altogether.
    • (Source) GNU tar attempts to use SEEK_DATA/SEEK_HOLE, then falls back on brute-force zero block detection.

    I'm not too fond of the OS specific things that they do to detect holes (granted archive/tar already has many OS specific things in it). I think it would be nice if tar.Writer provided a way to write spares files, but I think we should delegate detection of sparse holes to the user for now. If possible, we can try and get sparse info during FileInfoHeader, but I'm not sure that os.FileInfo has the necessary information to do the queries that are needed.

  11. self-assigned this
    on May 9, 2016
  12. AkihiroSuda commented on Nov 29, 2016

    @AkihiroSuda
    Contributor

    @dsnet Design SGTM (non-binding), do you plan to implement that feature?

  13. dsnet commented on Dec 1, 2016

    @dsnet
    Member

    I'll try and get this into the Go 1.9 cycle. However, a major refactoring of the tar.Writer implementation needs to happen first.

  14. modified the milestones: Go1.9Maybe, Unplanned on Dec 1, 2016
  15. 45 remaining items

  16. added this to the Unplanned milestone on Nov 17, 2017
  17. added
    FeatureRequestIssues asking for a new feature that does not need a proposal.
    on Nov 17, 2017
  18. added
    NeedsFixThe path to resolution is known, but the work has not been done.
    on Nov 17, 2017
  19. Gogowitsch commented on Feb 11, 2021

    @Gogowitsch

    Dear Go heros, please try to get sparse support into tar.Writer. Thanks!

  20. realtebo commented on Mar 29, 2024

    @realtebo
  21. jonathongardner commented on May 9, 2025

    @jonathongardner

    Is there any way to manually write sparse files? I figured if I put the PAXRecords with GNU.sparse... in manually and then write a gnu sparse formatted file with mapping etc. to the file part it would work but it seems the GNU.sparse headers are ignored...

  22. glycerine commented on Aug 31, 2025

    @glycerine

    Is there any way to manually write sparse files? I figured if I put the PAXRecords with GNU.sparse... in manually and then write a gnu sparse formatted file with mapping etc. to the file part it would work but it seems the GNU.sparse headers are ignored...

    @jonathongardner I'm not sure if I'm on the same page because I don't know what PAXRecords and GNU.sparse are referring to, but certainly from Go you can manually make sparse files. I have library and test suite that does that here (and there are others on github of varying quality).

    https://github.com/glycerine/rpc25519/tree/master/jsync/sparsified

    See also (especially for darwin/MacOS), my comments on a related ticket for some hard won advice on working with sparse files:

    #22735 (comment)

  23. johnnybubonic commented on Jan 20, 2026

    @johnnybubonic

    @glycerine -

    He is talking about the PAX sparse file extension 1.0, which would be a suitable solution to this entire issue if it worked - detecting sparse blocks in Linux is easy, but there's functionally no way to write them in a sparse tar format because the library is ignoring the header extensions.

    More accurately, it silently removes them.

    Attempting to manually make archive/tar write the 1.0 sparse format requires using tar.TypeXHeader for a tar.Header.Format - and attempting to do so explicitly of course returns a archive/tar: cannot encode header: cannot manually encode TypeXHeader, TypeGNULongName, or TypeGNULongLink headers error.

    If there's one thing that's insufferably infuriating, it's incomplete implementations in libraries that intentionally cripple any attempts of fully implementing. Guess it's time to fork out archive/tar; it's wild that the bikeshedding from the stdlib team surrounding this has led to not being able to implement a common and ubiquitous use case and spec established 20 years ago.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    FeatureRequestIssues asking for a new feature that does not need a proposal.NeedsFixThe path to resolution is known, but the work has not been done.

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions