Code Monkey home page Code Monkey logo

go-diskfs's Introduction

go-diskfs

go-diskfs is a go library for performing manipulation of disks, disk images and filesystems natively in go.

You can do nearly everything that go-diskfs provides using shell tools like gdisk/fdisk/mkfs.vfat/mtools/sgdisk/sfdisk/dd. However, these have the following limitations:

  • they need to be installed on your system
  • you need to fork/exec to the command (and possibly a shell) to run them
  • some are difficult to run without mounting disks, which may not be possible or may be risky in your environment, and almost certainly will require root privileges
  • you do not want to launch a VM to run the excellent libguestfs and it may not be installed

go-diskfs performs all modifications natively in go, without mounting any disks.

Usage

Note: detailed go documentation is available at godoc.org.

Concepts

go-diskfs has a few basic concepts:

  • Disk
  • Partition
  • Filesystem

Disk

A disk represents either a file or block device that you access and manipulate. With access to the disk, you can:

  • read, modify or create a partition table
  • open an existing or create a new filesystem

Partition

A partition is a slice of a disk, beginning at one point and ending at a later one. You can have multiple partitions on a disk, and a partition table that describes how partitions are laid out on the disk.

Filesystem

A filesystem is a construct that gives you access to create, read and write directories and files.

You do not need a partitioned disk to work with a filesystem; filesystems can be an entire disk, just as they can be an entire block device. However, they also can be in a partition in a disk

Working With a Disk

Before you can do anything with a disk - partitions or filesystems - you need to access it.

  • If you have an existing disk or image file, you Open() it
  • If you are creating a new one, usually just disk image files, you Create() it

The disk will be opened read-write, with exclusive access. If it cannot do either, it will fail.

Once you have a Disk, you can work with partitions or filesystems in it.

Partitions on a Disk

The following are the partition actions you can take on a disk:

  • GetPartitionTable() - if one exists. Will report the table layout and type.
  • Partition() - partition the disk, overwriting any previous table if it exists

As of this writing, supported partition formats are Master Boot Record (mbr) and GUID Partition Table (gpt).

Filesystems on a Disk

Once you have a valid disk, and optionally partition, you can access filesystems on that disk image or partition.

  • CreateFilesystem() - create a filesystem in an individual partition or the entire disk
  • GetFilesystem() - access an existing filesystem in a partition or the entire disk

As of this writing, supported filesystems include FAT32 and ISO9660 (a.k.a. .iso).

With a filesystem in hand, you can create, access and modify directories and files.

  • Mkdir() - make a directory in a filesystem
  • Readdir() - read all of the entries in a directory
  • OpenFile() - open a file for read, optionally write, create and append

Note that OpenFile() is intended to match os.OpenFile and returns a godiskfs.File that closely matches os.File

With a File in hand, you then can:

  • Write(p []byte) to the file
  • Read(b []byte) from the file
  • Seek(offset int64, whence int) to set the next read or write to an offset in the file

Read-Only Filesystems

Some filesystem types are intended to be created once, after which they are read-only, for example ISO9660/.iso and squashfs.

godiskfs recognizes read-only filesystems and limits working with them to the following:

  • You can GetFilesystem() a read-only filesystem and do all read activities, but cannot write to them. Any attempt to Mkdir() or OpenFile() in write/append/create modes or Write() to the file will result in an error.
  • You can CreateFilesystem() a read-only filesystem and write anything to it that you want. It will do all of its work in a "scratch" area, or temporary "workspace" directory on your local filesystem. When you are ready to complete it, you call Finalize(), after which it becomes read-only. If you forget to Finalize() it, you get... nothing. The Finalize() function exists only on read-only filesystems.

Example

There are examples in the examples/ directory. Here is one to get you started.

The following example will create a fully bootable EFI disk image. It assumes you have a bootable EFI file (any modern Linux kernel compiled with CONFIG_EFI_STUB=y will work) available.

import diskfs "github.com/diskfs/go-diskfs"

espSize int := 100*1024*1024 // 100 MB
diskSize int := espSize + 4*1024*1024 // 104 MB


// create a disk image
diskImg := "/tmp/disk.img"
disk := diskfs.Create(diskImg, diskSize, diskfs.Raw, diskfs.SectorSizeDefault)
// create a partition table
blkSize int := 512
partitionSectors int := espSize / blkSize
partitionStart int := 2048
partitionEnd int := partitionSectors - partitionStart + 1
table := PartitionTable{
	type: partition.GPT,
	partitions:[
		Partition{Start: partitionStart, End: partitionEnd, Type: partition.EFISystemPartition, Name: "EFI System"}
	]
}
// apply the partition table
err = disk.Partition(table)


/*
 * create an ESP partition with some contents
 */
kernel, err := os.ReadFile("/some/kernel/file")

fs, err := disk.CreateFilesystem(0, diskfs.TypeFat32)

// make our directories
err = fs.Mkdir("/EFI/BOOT")
rw, err := fs.OpenFile("/EFI/BOOT/BOOTX64.EFI", os.O_CREATE|os.O_RDRWR)

err = rw.Write(kernel)

Tests

There are two ways to run tests: unit and integration (somewhat loosely defined).

  • Unit: these tests run entirely within the go process, primarily test unexported and some exported functions, and may use pre-defined test fixtures in a directory's testdata/ subdirectory. By default, these are run by running go test ./... or just make unit_test.
  • Integration: these test the exported functions and their ability to create or manipulate correct files. They are validated by running a docker container with the right utilities to validate the output. These are run by running TEST_IMAGE=diskfs/godiskfs go test ./... or just make test. The value of TEST_IMAGE will be the image to use to run tests.

For integration tests to work, the correct docker image must be available. You can create it by running make image. Check the Makefile to see the docker build command used to create it. Running make test automatically creates the image for you.

Integration Test Image

The integration test image contains the various tools necessary to test images: mtools, fdisk, gdisk, etc. It works on precisely one file at a time. In order to avoid docker volume mounting limitations with various OSes, instead of mounting the image -v, it expects to receive the image as a stdin stream, and saves it internally to the container as /file.img.

For example, to test the existence of directory /abc on file $PWD/foo.img:

cat $PWD/foo.img | docker run -i --rm $INT_IMAGE mdir -i /file.img /abc

Plans

Future plans are to add the following:

  • embed boot code in mbr e.g. altmbr.bin (no need for gpt since an ESP with /EFI/BOOT/BOOT<arch>.EFI will boot)
  • ext4 filesystem
  • Joliet extensions to iso9660
  • Rock Ridge sparse file support - supports the flag, but not yet reading or writing
  • squashfs sparse file support - currently treats sparse files as regular files
  • qcow disk format

go-diskfs's People

Contributors

a2geek avatar abarisani avatar adamqqqplay avatar akhilerm avatar andrerenaud avatar carbonin avatar cusspvz avatar deitch avatar dwj300 avatar ekodian avatar elliotwutingfeng avatar guimagalhaes avatar iancoolidge avatar ibuildthecloud avatar jawn-smith avatar kayuii avatar leslie-qiwa avatar luthermonson avatar ncw avatar rgl avatar rpunt avatar rtreffer avatar shellwhale avatar sil2100 avatar stapelberg avatar thirdeyenick avatar tvories avatar vanackere avatar vielmetti avatar yadutaf avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

go-diskfs's Issues

Usage of int64 as return

First, Thanks for this cool library!

Question / Problem Statment :

I was trying to use this library to get starting offset values of partitions inside disk image from partition.GetStart()

func (p *Partition) GetStart() int64 {
	_, lss := p.sectorSizes()
	return int64(p.Start) * int64(lss)
}

I am not sure if I fully understand this or identifying a potential problem here due to the return value is int64 which could have negative integer numbers. However when working with partitions, If I understand correctly, cannot have a negative starting point and the minimum starting point is 0 so should not this be the return of uint64?

PS: I am a newbie in golang world. I could have missed something to understand.

Unable to write to FAT32 partition

Starting out with an image file containing BalenaOS for Raspberry Pi and trying to write to its boot partition (partition 1) to a file called config.json results in the following failure

Error writing directory entries to disk: Could not create a valid byte stream for a FAT32 Entries: Could not convert long filename to directory entries: Could not calculate checksum for 8.3 filename: Invalid shortname character in filename: IMAGE-~1

Note that we can read the file without issues to get its content.

A minimal example to reproduce the write error should be

package main

import (
	"bytes"
	"io"
	"log"
	"os"

	"github.com/diskfs/go-diskfs"
)

func main() {
	disk, err := diskfs.Open(os.Args[1])
	fatalf(err, "failed to open: %v", err)

	fs, err := disk.GetFilesystem(1)
	fatalf(err, "failed to get filesystem for partition %d: %v", 1, err)

	fp := "/config.json"

	file, err := fs.OpenFile(fp, os.O_CREATE|os.O_RDWR)
	fatalf(err, "failed to read file %q: %v", fp, err)

	buf := bytes.NewBuffer([]byte(`{"some":"json data"}`))

	if _, err := io.Copy(file, buf); err != nil {
		fatalf(err, "failed to write to file: %v", err)
	}
}

func fatalf(err error, format string, args ...interface{}) {
	if err == nil {
		return
	}
	log.Fatalf(format+"\n", args...)
}

which can be invoked with e.g. go run . balena-cloud-raspberrypi4-64-2.75.0+rev1-v12.5.10.img to produce the error.

Note though that as of writing the latest tagged/release module version does not contain certain bug fixes so installing from master (go get github.com/diskfs/go-diskfs@master) is required. Here is the go.mod and go.sum files I've been using, for a reproducible build.

module x

go 1.16

require (
	github.com/diskfs/go-diskfs v1.1.2-0.20210413082520-a81d74af0d46
	github.com/sirupsen/logrus v1.8.1 // indirect
)

4d63.com/gochecknoinits v0.0.0-20200108094044-eb73b47b9fc4/go.mod h1:4o1i5aXtIF5tJFt3UD1knCVmWOXg7fLYdHVu6jeNcnM=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/diskfs/go-diskfs v1.1.1 h1:rMjLpaydtXGVZb7mdkRGK1+//30i76nKAit89zUzeaI=
github.com/diskfs/go-diskfs v1.1.1/go.mod h1:afUPxxu+x1snp4aCY2bKR0CoZ/YFJewV3X2UEr2nPZE=
github.com/diskfs/go-diskfs v1.1.2-0.20210413082520-a81d74af0d46 h1:21hFKsHoGVwGdzjUMdNo1vISHjw45KounH+WkP4AXP8=
github.com/diskfs/go-diskfs v1.1.2-0.20210413082520-a81d74af0d46/go.mod h1:ZTeTbzixuyfnZW5y5qKMtjV2o+GLLHo1KfMhotJI4Rk=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gordonklaus/ineffassign v0.0.0-20190601041439-ed7b1b5ee0f8/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
github.com/jgautheron/goconst v0.0.0-20170703170152-9740945f5dcb/go.mod h1:82TxjOpWQiPmywlbIaB2ZkqJoSYJdLGPgAJDvM3PbKc=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mibk/dupl v1.0.0/go.mod h1:pCr4pNxxIbFGvtyCOi0c7LVjmV6duhKWV+ex5vh38ME=
github.com/pierrec/lz4 v2.3.0+incompatible h1:CZzRn4Ut9GbUkHlQ7jqBXeZQV41ZSKWFc302ZU6lUTk=
github.com/pierrec/lz4 v2.3.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/xattr v0.4.1 h1:dhclzL6EqOXNaPDWqoeb9tIxATfBSmjqL0b4DpSjwRw=
github.com/pkg/xattr v0.4.1/go.mod h1:W2cGD0TBEus7MkUgv0tNZ9JutLtVO3cXu+IBRuHqnFs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stripe/safesql v0.2.0/go.mod h1:q7b2n0JmzM1mVGfcYpanfVb2j23cXZeWFxcILPn3JV4=
github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9/go.mod h1:q+QjxYvZ+fpjMXqs+XEriussHjSYqeXVnAdSV1tkMYk=
github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181021155630-eda9bb28ed51/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8=
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20200102200121-6de373a2766c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/djherbis/times.v1 v1.2.0 h1:UCvDKl1L/fmBygl2Y7hubXCnY7t4Yj46ZrBFNUipFbM=
gopkg.in/djherbis/times.v1 v1.2.0/go.mod h1:AQlg6unIsrsCEdQYhTzERy542dz6SFdQFZFv6mUY0P8=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=

Some additional information that may be helpful is a view of the image file.

dev=$(sudo losetup --find --show --partscan balena-cloud-raspberrypi4-64-2.75.0+rev1-v12.5.10.img)
sudo mount ${dev}p1 /mnt
ls -l /mnt
total 6553
-rwxr-xr-x 1 root      24 Jan  1  2098 balena-image
-rwxr-xr-x 1 root   14002 Jan  1  2098 balenaos.fingerprint
-rwxr-xr-x 1 root   47770 Jan  1  2098 bcm2711-rpi-4-b.dtb
-rwxr-xr-x 1 root       0 Jan  1  2098 bootfiles-20201022.stamp
-rwxr-xr-x 1 root     516 Jan  1  2098 boot.scr
-rwxr-xr-x 1 root      95 Jan  1  2098 cmdline.txt
-rwxr-xr-x 1 root      89 Jan  1  2098 config.json
-rwxr-xr-x 1 root   36385 Jan  1  2098 config.txt
-rwxr-xr-x 1 root    2091 Jan  1  2098 device-type.json
-rwxr-xr-x 1 root       0 Jan  1  2098 extra_uEnv.txt
-rwxr-xr-x 1 root    3163 Jan  1  2098 fixup4cd.dat
-rwxr-xr-x 1 root    5417 Jan  1  2098 fixup4.dat
-rwxr-xr-x 1 root    8430 Jan  1  2098 fixup4x.dat
-rwxr-xr-x 1 root      44 Jan  1  2098 image-version-info
-rwxr-xr-x 1 root  569944 Jan  1  2098 kernel8.img
-rwxr-xr-x 1 root     205 Jan  1  2098 os-release
drwxr-xr-x 2 root   17920 Jan  1  2098 overlays
drwxr-xr-x 2 root     512 Jan  1  2098 splash
-rwxr-xr-x 1 root  785532 Jan  1  2098 start4cd.elf
-rwxr-xr-x 1 root 2225376 Jan  1  2098 start4.elf
-rwxr-xr-x 1 root 2985256 Jan  1  2098 start4x.elf
drwxr-xr-x 2 root     512 Jan  1  2098 system-connections

sudo umount /mnt
sudo losetup -d $dev

Note the file config.json is present.

It may very well be an issue with the image file rather than the library, but we've been unable to make any progress with this and was hoping someone could advice.

Happy to provide more information and context if necessary.

support creating 4K raw images via Create (or new function)?

Hi,

We have a bunch of code in snapd / Ubuntu Core around creating partitions / structures that now via the ubuntu-image rewrite uses this library (see https://github.com/canonical/ubuntu-image for details, specifically makeDisk() in common_states.go) to create a raw .img file for a volume that then gets flashed to a device (or in the case of our tests used in a VM)

It would be great both for creating images for consumption on physical media that has 4K physical sector size / block size and for our own VM based testing to be able to create disk images via some option that use 4K as the sector size instead of the default 512 that is currently hard-coded here in diskfs.go.

Maybe a new function CreateWithSectorSize or some additional Format flag in Create()?

Thanks,
Ian

Can not read filesystem in ISO9660 image with creation timestamp

When I create ISO-image with UltraISO program with any data and any image size, and set image creation (or/and modification) timestamp, this image can not be opened with this API. This image is well opened in Windows explorer, but cannot be read by API. Call to GetFilesystem(0) returns error "Unknown filesystem on partition 0". Creation timestamp can be set in UltraISO image properties in tab date/time. If timestamp is not set, it's all working.

ISO-image is opens with subsequent calls:

	if disk, err = diskfs.OpenWithMode(fpath, diskfs.ReadOnly); err != nil {
		return
	}
	if fs, err = disk.GetFilesystem(0); err != nil { // assuming it is the whole disk, so partition = 0
		return
	}

change uuid of disk and partition inside raw image

I have some sort of cloud hosting. Main problem now, that all os installed from already created images (say snapshots).and have identical uuid of disks and partitions, but when i'm attach to already booted server image with system from another server i have identical uuid of different disks and identical partition uuid.
What is the best way to avoid this? I think about after create image from snapshot modify gpt partition table and set empty uuid, so go-diskfs generate new.
May be i miss something?

undefined: unix.IoctlGetInt

I'm getting an error when trying to import this library in my go code.

# github.com/diskfs/go-diskfs
..\..\go\src\github.com\diskfs\go-diskfs\diskfs.go:313:28: undefined: unix.IoctlGetInt
..\..\go\src\github.com\diskfs\go-diskfs\diskfs.go:317:29: undefined: unix.IoctlGetInt

I am very new to go, so I could definitely be doing something wrong, but this is the first issue I've had with an import.

This is on a Windows 10 machine. I haven't tried yet with another OS.

create tagged releases for go-diskfs

As of now, to include go-diskfs as a dependency we need to refer to a specific commit in either Gopkg.toml or go.mod file.

Creating versioned release with semantic versioning v1.0.x will help to give a clean dependency in the Gopkg.toml or go.mod file.

Unable to truncate file in FAT32 filesystem

The image file BalenaOS for Raspberry Pi contains a file called /config.json in its boot partition (partition 1). When trying to overwrite the file contents by opening it with os.O_TRUNC this does not work. The initial file contents is the following.

{
  "deviceType": "raspberrypi4-64",
  "localMode": true,
  "persistentLogging": false
}

and after running a program that opens /config.json with the os.O_TRUNC flag and writes new content that is smaller than the existing content, e.g. {"some":"json data"}, the result is the following.

{"some":"json data"}aspberrypi4-64",
  "localMode": true,
  "persistentLogging": false
}

The new content has successfully been written over the first existing bytes in the file, but the previous latter bytes remain.

The following is a program to reproduce the issue on an image file. The image file is taken as the first argument.

main.go
package main

import (
	"bytes"
	"io"
	"log"
	"os"

	"github.com/diskfs/go-diskfs"
)

func main() {
	disk, err := diskfs.Open(os.Args[1])
	fatalf(err, "failed to open: %v", err)

	fs, err := disk.GetFilesystem(1)
	fatalf(err, "failed to get filesystem for partition %d: %v", 1, err)

	fp := "/config.json"

	file, err := fs.OpenFile(fp, os.O_CREATE|os.O_RDWR|os.O_TRUNC)
	fatalf(err, "failed to open file %q: %v", fp, err)

	buf := bytes.NewBuffer([]byte(`{"some":"json data"}`))

	if _, err := io.Copy(file, buf); err != nil {
		fatalf(err, "failed to write to file: %v", err)
	}
}

func fatalf(err error, format string, args ...interface{}) {
	if err == nil {
		return
	}
	log.Fatalf(format+"\n", args...)
}

Why is the file not truncated when it is opened, even though os.O_TRUNC is passed as a flag?

Let me know if more information is needed and I'm happy to help figure this one out.

Feature Request: Joliet and Rock Ridge extensions supporting

Good day
Hope you are doing well.

For now: Joliet and Rock Ridge extensions are not supported (I mean reading or writing).
As I see you have a plans:

  • Joliet extensions to iso9660
  • Rock Ridge sparse file support - supports the flag, but not yet reading or writing

Do you plan to write additional methods for this in near future? Maybe you know some ETA?

io.Copy error?

I'm probably doing something wrong here, so apologies in advance. However, when I copy a large file into a newly created fat32, the copied size is correct, but the tail of the file is zeroed out.

Using the following as a fat32.go test file: https://gist.github.com/hallyn/24acd9c7cf407c0561629b5c032f2bb8

With the following go.mod:

module github.com/hallyn/disktest

require (
    #github.com/diskfs/go-diskfs v1.2.0
    github.com/diskfs/go-diskfs v0.0.0-20211001121417-bcaf6e5e95e0
)

go 1.17

I fetched
https://github.com/shadow-maint/shadow/releases/download/v4.9/shadow-4.9.tar.gz
and copied it to /tmp/in, and ran 'go run fat32.go'. The test places the output
in /tmp/fat, so

serge@sl ~/test/go-disk$ diff /mnt/EFI/BOOT/kernel.efi  /tmp/in
Binary files /mnt/EFI/BOOT/kernel.efi and /tmp/in differ
serge@sl ~/test/go-disk$ od -x /tmp/in > /tmp/1
oserge@sl ~/test/go-disk$ od -x /mnt/EFI/BOOT/kernel.efi > /tmp/2
serge@sl ~/test/go-disk$ diff -up /tmp/1 /tmp/2
--- /tmp/1	2021-10-13 22:54:31.010732470 -0500
+++ /tmp/2	2021-10-13 22:54:37.782968885 -0500
@@ -247614,15 +247614,7 @@
 17071720 ce36 091d e94a f314 f129 ed8b 3bac 9276
 17071740 bed6 fb3e 3266 fb35 9e22 a7d6 7f44 f10b
 17071760 4c2f 0a45 abfc 4b98 8e50 199e 4cae e0a9
-17072000 cade de09 4c98 b359 466e f0fb eb3b 41e3
-17072020 4726 4ff9 0597 7fc9 01ac f7dc 5ef4 39b6
-17072040 b8f0 a181 27e3 4430 837d f9ae d9b7 4219
-17072060 f295 ddd0 4d92 2460 7938 b618 5720 fef6
-17072100 121d 9a6e 598f 94c8 76a3 ed09 fbdc 8fa0
-17072120 a4a2 9b6f 3cab 2de1 23cf 50ef 3e75 3fa1
-17072140 639c 003b 6318 62b7 42b9 614a 06db f011
-17072160 149e 8ee3 88be a95c 2523 aa58 7a75 f5ad
-17072200 facf fd67 feb3 ff59 7fac 3fd6 9feb cff5
-17072220 67fa b3fd 59fe acff d67f eb3f 5f9f e7fd
-17072240 01ff 4005 874f 0800 011f
+17072000 0000 0000 0000 0000 0000 0000 0000 0000
+*
+17072240 0000 0000 0000 0000 0000
 17072252

fat32 labels

When creating a fat32 filesystem with a volume label it doesn't label it correctly

cloudInitFS, err := destDisk.CreateFilesystem(disk.FilesystemSpec{
  Partition:   1,
  FSType:      filesystem.TypeFat32,
  VolumeLabel: "config-2",
})

Running blkid shows that the label applied is LABEL_FATBOOT="config-2" instead of LABEL="config-2".

When I run the fatlabel utility to add a label manually it does it correctly by setting LABEL as well.

fat32.File does not not implement io.Writer correctly.

It seems that multiple calls to fat32.File.Write() will simply cause previous writes to be overwritten. This is pretty noticeable if one tries to call io.Copy a large reader (32KB+) into it:

var src os.File = io.Open(...)
dest, _ = destfs.OpenFile(path, os.O_CREATE|os.O_RDWR)
written, err := io.Copy(dest, src) 

In this scenario io.Copy will report all src bytes as written and no error is reported. The final file will contain only the last 32KB of the original file (that's because 32KB is the default buffer size used by io.Copy).

Bootable iso example

I'm trying to create a bootable iso. The gist of it, is I want to recreate this command:

genisoimage \
   -V my-volume \
   -c isolinux/boot.cat \
   -b isolinux/isolinux.bin \
   -no-emul-boot \
   -boot-load-size 4 \
   -boot-info-table \
   -eltorito-alt-boot \
   -e images/efiboot.img \
   -no-emul-boot \
   -o test-iso.iso \
   /tmp/iso-test/

I think the issue is in my finalize options which currently look like this:

options := iso9660.FinalizeOptions{
        VolumeIdentifier: "my-volume",
        RockRidge:        true,
        ElTorito: &iso9660.ElTorito{
                BootCatalog: "isolinux/boot.cat",
                Entries: []*iso9660.ElToritoEntry{
                        &iso9660.ElToritoEntry{
                                Platform:    iso9660.BIOS,
                                Emulation:   iso9660.NoEmulation,
                                BootFile:    "isolinux/isolinux.bin",
                                LoadSegment: 4,
                        },      
                        &iso9660.ElToritoEntry{
                                Platform:  iso9660.EFI,
                                Emulation: iso9660.NoEmulation,
                                BootFile:  "images/efiboot.img",
                        },      
                },
        },        
}

After running my code I get an iso that file shows as bootable but doesn't actually boot in a VM:
image

isoinfo -i test-iso.iso -d shows:

    Eltorito validation header:
    Hid 1
    Arch 0 (x86)
    ID 'https://github.com/disk'
    Key 55 AA
    Eltorito defaultboot header:
        Bootid 88 (bootable)
        Boot media 0 (No Emulation Boot)
        Load segment 4
        Sys type 0
        Nsect 4C
        Bootoff 0 0

Any hints?

false physical blocksize detected

I have a cdrom in a VM that is emulated from an iso on the host system.

in the VM, fdisk reports these block sizes:

$ sudo fdisk -l /dev/sr0 
Disk /dev/sr0: 4 MiB, 4194304 bytes, 2048 sectors
Disk model: QEMU DVD-ROM    
Units: sectors of 1 * 2048 = 2048 bytes
Sector size (logical/physical): 2048 bytes / 2048 bytes
I/O size (minimum/optimal): 2048 bytes / 2048 bytes

while diskfs is detecting a different physical block size:

disk, _ := diskfs.OpenWithMode("/dev/sr0", diskfs.ReadOnly)
fs, _ := disk.GetFilesystem(0)
DEBU[0000] checking device: /dev/sr0                    
DEBU[0000] initDisk(): start                            
DEBU[0000] initDisk(): block device                     
DEBU[0000] initDisk(): logical block size 2048, physical block size 4096 
DEBU[0000] trying fat32                                 
DEBU[0000] fat32 failed: blocksize for FAT32 must be either 512 bytes or 0, not 2048 
DEBU[0000] trying iso9660 with physical block size 4096 

what could cause this? is there something i can tune so this can't happen?


related to #103

Function for isoinfo-like data

Is there a way to use this library to get data similar to what is given by isoinfo -lR -i test.iso?

Sample output:

Directory listing of /
dr-xr-xr-x   5    0    0            2048 Oct  9 2020 [     29 02]  . 
dr-xr-xr-x   5    0    0            2048 Oct  9 2020 [     29 02]  .. 
dr-xr-xr-x   3    0    0            2048 Oct  9 2020 [     33 02]  EFI 
dr-xr-xr-x   3    0    0            2048 Oct  9 2020 [     30 02]  images 
dr-xr-xr-x   2    0    0            2048 Oct  9 2020 [     32 02]  isolinux 
-r--r--r--   1    0    0             132 Oct  9 2020 [   4053 00]  zipl.prm 
...
Directory listing of /images/pxeboot/
dr-xr-xr-x   2    0    0            2048 Oct  9 2020 [     31 02]  . 
dr-xr-xr-x   3    0    0            2048 Oct  9 2020 [     30 02]  .. 
-r--r--r--   1    0    0        80239428 Oct  9 2020 [   4182 00]  initrd.img 
-r--r--r--   1    0    0       819969024 Oct  9 2020 [  43362 00]  rootfs.img 
-r--r--r--   1    0    0         8924528 Oct  9 2020 [ 443738 00]  vmlinuz 

Specifically I'm looking to get the file size and offset without first unpacking the image.

Go modules

I propose to move dependency management to go modules. Benefits:

  • one less extra tool needed (i.e. dep), as module management is embedded into go now
  • development in a GOPATH-independent environment

fat32.Filesystem fails silently when writing files to a directory with many items

I'm trying to write a program that formats an SD card and writes the contents of https://github.com/raspberrypi/firmware/tree/master/boot/ to it. The core of the code is:

	d, _ := diskfs.Open(dev)
	sectorSize := 512
	ptable := &mbr.Table{
		LogicalSectorSize:  sectorSize,
		PhysicalSectorSize: sectorSize,
		Partitions: []*mbr.Partition{
			{
				Type:  mbr.Fat32LBA,
				Start: 4 * MiB / uint32(sectorSize),
				Size:  2 * GiB / uint32(sectorSize),
			},
		},
	}
	_ := d.Partition(ptable)
	fs, _ := d.CreateFilesystem(disk.FilesystemSpec{
		Partition: 1, FSType: filesystem.TypeFat32, 
	})
	for _, file := range files {
		dest, _ := fs.OpenFile(...)
		_, _ = dest.Write(file.data)
	}

Error handling has been abbreviated as well as directory creation, but both are there in my code.

Once I try to write the files in https://github.com/raspberrypi/firmware/tree/master/boot/overlays (219 entries) to /overlays the code does not report any errors, however only the first ~50-ish files are properly written. Files that are supposedly written later, but to a different directory, are still written correctly.

IMHO this is particularly bad because both fs.OpenFile and dest.Write fail silently, so the caller/user will just notice this much after the program has executed.

fat32 read example?

Read the img file, read the MBR, and find the partition type 0x0c, which is not recognized by fat32.Read!

Can you help me?
mbr github.com/rekby/mbr

Error reading MS-DOS Boot Sector: Could not read FAT32 BIOS Parameter Block from boot sector: Invalid FAT32 version found: 20041

func test() error{
f, _ := os.Open("F:\Raspberry Pi4\2020-08-20-raspios-buster-arm64.img")
Mbr, err := mbr.Read(f)
if err != nil {
return err;
}
if(Mbr.IsGPT()){
return errors.New("no mbr");
}
parts:=Mbr.GetAllPartitions()
var i=0;
for i=0;i<len(parts);i++{
part:=parts[i]
fmt.Printf("i:%d type:%d size:%d\r\n",i,part.GetType(),part.GetLBALen())
if(part.GetType()==0x0c){
fmt.Printf("start:%d %d \r\n",int64(part.GetLBAStart()),part.GetLBALast())
fs, err := fat32.Read(f, int64(part.GetLBALen()), int64(part.GetLBAStart()*512), 512)
fmt.Printf("err:%v\r\n",err)
}
}
f.Close()
return nil;
}

cannot read filesystem on a partition without a partition table

I am attempting to list the contents of a FAT32 partition from the Raspberry Pi Os Lite image file. Fdisk shows the partition number as 1 for the FAT partition:

fdisk ~/Documents/gits/tmd/rpi.img                                   
Disk: /Users/tvories/Documents/gits/tmd/rpi.img	geometry: 893/64/63 [3604480 sectors]
Signature: 0xAA55
         Starting       Ending
 #: id  cyl  hd sec -  cyl  hd sec [     start -       size]
------------------------------------------------------------------------
 1: 0C   64   0   1 - 1023   3  32 [      8192 -     524288] Win95 FAT32L
 2: 83 1023   3  32 - 1023   3  32 [    532480 -    3072000] Linux files*
 3: 00    0   0   0 -    0   0   0 [         0 -          0] unused
 4: 00    0   0   0 -    0   0   0 [         0 -          0] unused

I can mount the disk image on my mac and read access this partition. When I try to read the contents of this directory using go-diskfs, I get this error:

go run main.go list
2020/11/30 13:52:56 cannot read filesystem on a partition without a partition table
panic: cannot read filesystem on a partition without a partition table

Here is my code:

func readFs() {
    path, err := os.Getwd() // gets current working directory

		if err != nil {
			log.Panic(err)
		}

		disk, err := diskfs.Open(path + "/rpi.img")
		if err != nil {
			log.Panic(err)
		}

		fs, err := disk.GetFilesystem(1) // Trying to get contents of partition ID 1
		if err != nil {
			log.Panic(err)
		}

		files, err := fs.ReadDir("/")
		if err != nil {
			log.Panic(err)
		}

		fmt.Println(files)
}

The partition table shows up as MBR:

 gpt -r show /dev/disk2                                 
    start     size  index  contents
        0        1         MBR
        1     8191
     8192   524288      1  MBR part 12
   532480  3072000      2  MBR part 131

Is this an issue with the img file or am I doing something wrong?

Cannot create partition at end of disk then make directory

When creating a fat32 partition at the end of the disk and then creating a directory in that partition. If the partition is then mounted nothing is showing in the partition.

It also seems to only happen at weird disk to partition size ratios.

  • Less than 5GB any partition size works
  • At 5GB partitions needs to be 2GB or bigger

ect... as the disk grows the partitions need to be larger and larger for the directory to show up when mounting.

# When the disk is 5GB and the partition is 1GB
[root@fedora-s-1vcpu-1gb-sfo2-01 end]# go run main.go foo.img
2020/01/26 17:13:01 Writing partition table to disk
2020/01/26 17:13:01 Creating cloud init directory structure
[root@fedora-s-1vcpu-1gb-sfo2-01 end]# fdisk -lu foo.img 
Disk foo.img: 5 GiB, 5368709120 bytes, 10485760 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x00000000

Device     Boot   Start      End Sectors Size Id Type
foo.img1        8388608 10485759 2097152   1G 83 Linux
[root@fedora-s-1vcpu-1gb-sfo2-01 end]# mount -o loop,offset=4294967296 foo.img /mnt 
[root@fedora-s-1vcpu-1gb-sfo2-01 end]# ls -la /mnt
total 8
drwxr-xr-x.  2 root root 4096 Jan  1  1970 .
dr-xr-xr-x. 19 root root 4096 Jan 26 16:46 ..


# When the disk is 5GB and the partition is 2GB
[root@fedora-s-1vcpu-1gb-sfo2-01 end]# go run main.go foo.img
2020/01/26 17:14:33 Writing partition table to disk
2020/01/26 17:14:33 Creating cloud init directory structure
[root@fedora-s-1vcpu-1gb-sfo2-01 end]# fdisk -lu foo.img 
Disk foo.img: 5 GiB, 5368709120 bytes, 10485760 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x00000000

Device     Boot   Start      End Sectors Size Id Type
foo.img1        6291456 10485759 4194304   2G 83 Linux
[root@fedora-s-1vcpu-1gb-sfo2-01 end]# mount -o loop,offset=3221225472 foo.img /mnt 
[root@fedora-s-1vcpu-1gb-sfo2-01 end]# ls -la /mnt
total 12
drwxr-xr-x.  3 root root 4096 Jan  1  1970 .
dr-xr-xr-x. 19 root root 4096 Jan 26 16:46 ..
drwxr-xr-x.  3 root root 4096 Jan 26 17:14 openstack
package main

import (
	"fmt"
	"log"
	"os"
	"path"

	diskfs "github.com/diskfs/go-diskfs"
	"github.com/diskfs/go-diskfs/disk"
	"github.com/diskfs/go-diskfs/filesystem"
	"github.com/diskfs/go-diskfs/partition/mbr"
)

func check(err error) {
	if err != nil {
		log.Fatal(err)
	}
}

func CreateFSAndDir(diskImg string) {
	if diskImg == "" {
		log.Fatal("must have a valid path for diskImg")
	}
	mydisk, err := diskfs.Open(diskImg)
	if err != nil {
		var diskSize int64
		diskSize = 5 * 1024 * 1024 * 1024 // 5 GB
		mydisk, err = diskfs.Create(diskImg, diskSize, diskfs.Raw)
		check(err)
	}

	cloudInitSize := 1 * 1024 * 1024 * 1024 // 1 GB
	cloudInitSectors := uint32(cloudInitSize / 512)
	// we want to create it at the end of the disk
	// so find the disk sector count and minus the cloudinit sectors
	cloudInitStart := uint32(int(mydisk.Size)/512) - cloudInitSectors

	table := &mbr.Table{
		LogicalSectorSize:  512,
		PhysicalSectorSize: 512,
		Partitions: []*mbr.Partition{
			{
				Bootable: false,
				Type:     mbr.Linux,
				Start:    cloudInitStart,
				Size:     cloudInitSectors,
			},
		},
	}

	log.Print("Writing partition table to disk")
	err = mydisk.Partition(table)
	check(err)

	fspec := disk.FilesystemSpec{Partition: 1, FSType: filesystem.TypeFat32, VolumeLabel: "config-2"}
	fs, err := mydisk.CreateFilesystem(fspec)
	check(err)

	cloudInitPrefix := path.Join("/", "openstack", "latest")
	// place down cloud-init info
	log.Print("Creating cloud init directory structure")
	err = fs.Mkdir(cloudInitPrefix)
	if err != nil {
		log.Fatalf("Error creating cloud init directory structure: %v", err)
	}
}

func main() {
	if len(os.Args) < 2 {
		fmt.Printf("Usage: %s <outfile>\n", os.Args[0])
		os.Exit(1)
	}
	CreateFSAndDir(os.Args[1])
}

iso finalize leaks the workspace temp directory

Currently this library leaks temp directories when used as shown in the iso_create.go example

The Finalize method also removes the reference to the workspace, so what is the best practice here?

Should the example be changed to something like this:

diff --git a/examples/iso_create.go b/examples/iso_create.go
index c0983f8..73c6f91 100644
--- a/examples/iso_create.go
+++ b/examples/iso_create.go
@@ -33,6 +33,9 @@ func CreateIso(diskImg string) {
        if !ok {
                check(fmt.Errorf("not an iso9660 filesystem"))
        }
+       ws := fs.Workspace()
        err = iso.Finalize(iso9660.FinalizeOptions{})
        check(err)
+       err = os.RemoveAll(ws)
+       check(err)
 }

Or should finalize remove the workspace before it clears the reference in the filesystem struct?

I'll make whichever change you think makes more sense @deitch

diskfs.Open(...) opens the file in read-only mode

This is mostly a question on the design: I have code that opens an existing disk image with disk, err := diskfs.Open(c.diskFileName). That works fine. But as soon as I try to update a file on that disk, my code encounters write errors. Looking at the factory methods, I noticed that diskfs.Create(...) opens in os.O_RDWR mode but diskfs.Open(...) opens in os.O_RDONLY mode.

Is there some other mechanism to open the disk/image in read-write mode to enable changes? My code is using the FAT32 filesystem, so it should be a valid use case.

For the short-term, since I have a local copy, I just updated the diskfs.Open(...) code.

Thanks!

assignment mismatch for uuid.NewV4

Hi,
when I try to install the package I receive following Error(s):

$ go get github.com/diskfs/go-diskfs
# github.com/diskfs/go-diskfs/partition/gpt
../../../go/src/github.com/diskfs/go-diskfs/partition/gpt/partiton.go:218:8: assignment mismatch: 1 variable but uuid.NewV4 returns 2 values
../../../go/src/github.com/diskfs/go-diskfs/partition/gpt/table.go:82:22: multiple-value uuid.NewV4() in single-value context
../../../go/src/github.com/diskfs/go-diskfs/partition/gpt/table.go:283:8: assignment mismatch: 1 variable but uuid.NewV4 returns 2 values

uuid.V4 returns (uuid, error) which cannot be assigned to one var.

My quick and dirty fix is (just ignoring the errors):

diff --git a/partition/gpt/partiton.go b/partition/gpt/partiton.go
index e573b97..b8e0c02 100644
--- a/partition/gpt/partiton.go
+++ b/partition/gpt/partiton.go
@@ -215,7 +215,7 @@ func (p *Partition) initEntry(blocksize uint64, starting uint64) error {
        var guid uuid.UUID
 
        if part.GUID == "" {
-               guid = uuid.NewV4()
+               guid, _ = uuid.NewV4()
        } else {
                var err error
                guid, err = uuid.FromString(part.GUID)
diff --git a/partition/gpt/table.go b/partition/gpt/table.go
index e40330b..38a5e96 100644
--- a/partition/gpt/table.go
+++ b/partition/gpt/table.go
@@ -79,7 +79,8 @@ func (t *Table) initTable(size int64) {
                t.primaryHeader = 1
        }
        if t.GUID == "" {
-               t.GUID = uuid.NewV4().String()
+               guid, _ := uuid.NewV4()
+               t.GUID = guid.String()
        }
        if t.partitionArraySize == 0 {
                t.partitionArraySize = 128
@@ -280,7 +281,7 @@ func (t *Table) toGPTBytes(primary bool) ([]byte, error) {
        // 16 bytes disk GUID
        var guid uuid.UUID
        if t.GUID == "" {
-               guid = uuid.NewV4()
+               guid, _ = uuid.NewV4()
        } else {
                var err error
                guid, err = uuid.FromString(t.GUID)

Is this a error in the package or did make something wrong?

Cheers,
Kurt

Make iso workspace base dir configurable

Currently, when building an iso, you have to create the filesystem which creates for you a workspace directory which is always in the system temp dir.

Allowing this to be configurable somehow could solve two issues:

  1. I don't want to write to /tmp
    • Maybe I have more storage elsewhere or something faster
    • In my case specifically I want all of these writes to go to a volume rather than the container filesystem I'm running on
  2. I don't want to double write all my files
    • My use case is: extract an iso to dir x, make changes in x, recreate an iso from all the files rooted at x
    • If I could set the workspace to x when creating the filesystem I would save on all the calls I'm making to recreate the whole directory tree in the workspace

As for how to solve this ... it would be tough to do both without making a breaking API change to iso9660.Create so I understand if we don't want to do that.
An alternative option is to create a convenience function specifically in the iso9660 package that could create and finalize an iso in one call given the workspace dir (pre-populated with the iso content), the existing parameters to disk.Create (or some subset) and the finalize options.

Effectively I would contribute what I have here altered for general use.

@deitch what do you think? Is this something that you think would be useful or would you rather add a parameter somewhere for the workspace base dir?

Document how to read squashfs files

Hi, thanks for this very useful library. Could you please document how to read squashfs files and print the files, directories, and symlinks contained therein; e.g., is the following halfway right?

package main

import (
	"log"
	"os"

	"github.com/diskfs/go-diskfs/filesystem/squashfs" // Have to use: GO111MODULE=on /usr/local/go/bin/go get github.com/diskfs/go-diskfs@squashfs
)

func main() {
	f, err := os.Open("/home/me/go/src/github.com/diskfs/go-diskfs/filesystem/squashfs/testdata/file.sqs")
	if err != nil {
		log.Println(err)
	}
	defer f.Close()
	fs, err := squashfs.Read(f, 0, 0, 4096)
	if err != nil {
		log.Println(err)
	}
	files, err := fs.ReadDir("/") // How to list all contents including subdirectories?
	for f := range files {
		print(f)
	}
}

Panic runtime error invalid memory with MBR partition on disk

Hi,
I'm getting a panic: runtime error: invalid memory address when creating a MBR partition on a real disk.

Error:

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x4d1256]

goroutine 1 [running]:
github.com/diskfs/go-diskfs/disk.(*Disk).Partition(0x0, 0x536d60, 0xc000070690, 0x536520, 0xc000010640)
        /root/go/pkg/mod/github.com/diskfs/[email protected]/disk/disk.go:55 +0x26
main.main()

Sample code:

	diskImg := "/dev/sdb"
	// disk, _ := diskfs.Create(diskImg, diskSize, diskfs.Raw)

	disk, _ := diskfs.Open(diskImg)

	table := &mbr.Table{
		LogicalSectorSize:  512,
		PhysicalSectorSize: 512,
		Partitions: []*mbr.Partition{
			{
				Bootable: false,
				Type:     mbr.Linux,
				Start:    2048,
				Size:     20480,
			},
		},
	}

	disk.Partition(table)

Disks:

[root@s-daba flash1]# lsblk
NAME   MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda      8:0    0  40G  0 disk 
└─sda1   8:1    0  40G  0 part /
sdb      8:16   0   5G  0 disk

Manually creating MBR Partition table using fdisk on /dev/sdb works as expected. Also, changing diskImg to /tmp/foo.img also proceeds successfully.

Recreating partition table and filesystems results in errors when writing directorys/files

I have the following code and when I run it twice I get this error when creating the directory the 2nd time. Failed to read directory /openstack. fdisk shows the partition having the correct sectors and when I mount the partition the /openstack directory is corrupt and shows ??? when I ls -la the mount.

Any ideas?

I can "fix" it by manually formatting the partition using mkfs.xfs then I am able to run the code again. My guess is something is staying around causing the fat32 filesystem to be confused.

log.Printf("Reading disk %s", diskName)
destDisk, err := diskfs.Open(diskName)
if err != nil {
  log.Fatalf("Error opening disk %s: %v", diskName, err)
}

startSector := uint32(2048)
cloudInitSize := 1 * 1024 * 1024 * 1024 // 1 GB
cloudInitSectors := uint32(cloudInitSize / int(destDisk.LogicalBlocksize))

table := &mbr.Table{
  LogicalSectorSize:  int(destDisk.LogicalBlocksize),
  PhysicalSectorSize: int(destDisk.PhysicalBlocksize),
  Partitions: []*mbr.Partition{
    {
      Bootable: false,
      Type:     mbr.Linux,
      Start:    startSector,
      Size:     cloudInitSectors,
    },
  },
}

log.Print("Writing partition table to disk")
err = destDisk.Partition(table)
if err != nil {
  log.Fatalf("Error writing partition table to disk %s: %v", diskName, err)
}

log.Print("Creating cloud init filesystem")
cloudInitFS, err := destDisk.CreateFilesystem(disk.FilesystemSpec{
  Partition:   1,
  FSType:      filesystem.TypeFat32,
  VolumeLabel: "config-2",
})
if err != nil {
  log.Fatalf("Error creating cloud-init filesystem on %s: %v", diskName, err)
}

cloudInitPrefix := path.Join("/", "openstack", "latest")
// place down cloud-init info
log.Print("Creating cloud init directory structure")
err = cloudInitFS.Mkdir(cloudInitPrefix)
if err != nil {
  log.Fatalf("Error creating cloud init directory structure: %v", err)
}

In certain circumstances, go-diskfs can't read the final SUSP entry

Go-diskfs can miss the final System Use Entry in a System Use Field or a Continuation Area if the final entry is 4 bytes long.

You can see an example of this in Openstep4-Pr1User.iso inside this archive: https://archive.org/download/NeXTOSIMAGES/NeXT_NEXTSTEP_4.0_PR1_(beta).rar.

Try using go-diskfs to list the contents of the rr_moved directory in the above image.

Expected behavior

The directory appears empty because all directory entries have the RE system use entry.

Actual behavior

The directory does not appear empty. Go-diskfs misses some RE system use entries, which are 4 bytes in length, when they are the final entry in the system use field.

The cause

This is likely due to a mistake on directoryentrysystemuseextension.go:514.

The line currently reads

for i := 0; i+4 < len(b); {

I believe it should read

for i := 0; i+3 < len(b); {

A four byte field would take up i+0, i+1, i+2, and i+3, so as long as i+3 is less then len(b), we should be able to read.

I made the same mistake in my own implementation, which is how I found this.

dashes (-) in filenames on iso9660 images

Hi,
I'm not sure why, but a regexp in filesystem/iso9660/finalize.go and filesystem/iso9660/directoryentry.go sanitizes the filenames and replaces dashes (-) with underscores (_).
Afaik is a dash in a filename valid.
I changed this, added a '-' to regexp, and it looks good and works.
Should I create a PR or is there any risk in my solution?
Thanks,
Kurt

Allow writing sparse disk images

I'm trying to generate sparse disk images where I write a partition table, but the data I write into the partitions is much smaller than the partition size. Unfortunately, when I tried this with go-diskfs, the WriteContents function in gpt's partiton.go file checks that the written data matches the partition size. Can we relax this check so it checks that written size <= partition size?

Device size is zero when using blockdevice.

What I did
I tried to create a single GPT partition that spans the entire drive on a USB drive, using the below code.

func main() {
	diskPath := "/dev/sdb"
	disk, err := diskfs.Open(diskPath)
	if err != nil {
		fmt.Println("Error opening:", err)
	}
	endSector := (disk.Size / 512) - 34 // 34 is length of efi label
	t := &gpt.Table{
		Partitions: []*gpt.Partition{
			{
				Start: 2048,
				End:   uint64(endSector),
				Type:  gpt.LinuxFilesystem,
				},
		},
	}
	err = disk.Partition(t)
	if err != nil {
		fmt.Println("error partitioning : ", err)
	}
}

but the program exits with Error:

error partitioning :  Failed to write partition table: Error writing secondary partition array to disk: writeat /dev/sdb: negative offset

What might be the problem:
When the code was run in debug mode, found out that devInfo here is having the size as zero.
When the call to create a partition is made here, the empty d.Info.Size() is passed causing the error to occur.

Proposed solution:
replace d.Info.Size() with d.Size here.
When this change was made go-diskfs worked as expected and the partition was created.

Additional Info

  • OS
akhil@MayaData:~$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="18.04.4 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.4 LTS"
VERSION_ID="18.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic
  • Device on which I tried to create partition
akhil@MayaData:~$ udevadm info /dev/sdb
P: /devices/pci0000:00/0000:00:14.0/usb2/2-3/2-3:1.0/host2/target2:0:0/2:0:0:0/block/sdb
N: sdb
S: disk/by-id/usb-Kingston_DataTraveler_3.0_50E5495131BBB060892FBC8E-0:0
S: disk/by-path/pci-0000:00:14.0-usb-0:3:1.0-scsi-0:0:0:0
E: DEVLINKS=/dev/disk/by-id/usb-Kingston_DataTraveler_3.0_50E5495131BBB060892FBC8E-0:0 /dev/disk/by-path/pci-0000:00:14.0-usb-0:3:1.0-scsi-0:0:0:0
E: DEVNAME=/dev/sdb
E: DEVPATH=/devices/pci0000:00/0000:00:14.0/usb2/2-3/2-3:1.0/host2/target2:0:0/2:0:0:0/block/sdb
E: DEVTYPE=disk
E: ID_BUS=usb
E: ID_DRIVE_THUMB=1
E: ID_INSTANCE=0:0
E: ID_MODEL=DataTraveler_3.0
E: ID_MODEL_ENC=DataTraveler\x203.0
E: ID_MODEL_ID=1666
E: ID_PATH=pci-0000:00:14.0-usb-0:3:1.0-scsi-0:0:0:0
E: ID_PATH_TAG=pci-0000_00_14_0-usb-0_3_1_0-scsi-0_0_0_0
E: ID_REVISION=PMAP
E: ID_SERIAL=Kingston_DataTraveler_3.0_50E5495131BBB060892FBC8E-0:0
E: ID_SERIAL_SHORT=50E5495131BBB060892FBC8E
E: ID_TYPE=disk
E: ID_USB_DRIVER=usb-storage
E: ID_USB_INTERFACES=:080650:
E: ID_USB_INTERFACE_NUM=00
E: ID_VENDOR=Kingston
E: ID_VENDOR_ENC=Kingston
E: ID_VENDOR_ID=0951
E: MAJOR=8
E: MINOR=16
E: SUBSYSTEM=block
E: TAGS=:systemd:
E: USEC_INITIALIZED=159230551555
  • Go version
akhil@MayaData:~$ go version
go version go1.13.1 linux/amd64
akhil@MayaData:~$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/akhil/.cache/go-build"
GOENV="/home/akhil/.config/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/akhil/Work/Golang"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build076484881=/tmp/go-build -gno-record-gcc-switches"

ext4 support

This looks like a pretty amazing library and looks like an ideal package to form the basis of my installer for @void-linux. I'd need ext4 though to do this (and someday I'd love to see support for more exotic filesystems, even it if meant shelling out to their tools.

Do you foresee ext4 landing any time soon?

dependency "github.com/ulikunitz/xz @0.5.6" has govuln GO-2020-0016, CVE-2021-29482

The upstream issue is ulikunitz/xz#35 and the issue is resolved in ulikunitz/xz @0.5.8 and above.

The advisory at GHSA-25xm-hr59-7c27 reads:

The function readUvarint used to read the xz container format may not terminate a loop provide malicous input.

Found with deps.dev at https://deps.dev/go/github.com%2Fdiskfs%2Fgo-diskfs/v1.2.0

The most likely recommendation is to bring the dependency up to the current release. Risk seems low, as this fix has already been implemented by hashicorp/go-getter#279 and rhysd/go-github-selfupdate#36

Command line utility

Hi,

that library looks very interesting to me. Working in the the field of emedded Linux we always struggle with manipulating filesystems within CI/CD when Docker comes into play. Currently we need to rund a Docker image in privileged mode to loop mount the images.

Are there any plans for a command line utility which provides functionality to manipulate filesystems on disk? That would be super awesome for dealing with them inside of an none privileged Docker container.

Disk.Table is not populated without partition write

It is useful to be able to open a 'Disk' structure for reading, such as in a unit test.

this is not possible currently, as Disk.Table is only populated when writing a partition table to disk, which is a potentially destructive operation.

it should be possible to poulate Disk.Table from a partition read.

[iso9660] Index out of range panic

I'm getting this panic as part of running the linuxkit metadata service

panic: runtime error: index out of range [0] with length 0

goroutine 1 [running]:
github.com/linuxkit/linuxkit/pkg/metadata/vendor/github.com/diskfs/go-diskfs/filesystem/iso9660.parseDirEntry(0xa60458, 0x0, 0x0, 0xc0000e4000, 0x17000, 0x0, 0x0)
	/go/src/github.com/linuxkit/linuxkit/pkg/metadata/vendor/github.com/diskfs/go-diskfs/filesystem/iso9660/directoryentry.go:281 +0x789
github.com/linuxkit/linuxkit/pkg/metadata/vendor/github.com/diskfs/go-diskfs/filesystem/iso9660.Read(0x818500, 0xc00000e068, 0x400000, 0x0, 0x1000, 0x1, 0x8122e0, 0xc000010ed0)
	/go/src/github.com/linuxkit/linuxkit/pkg/metadata/vendor/github.com/diskfs/go-diskfs/filesystem/iso9660/iso9660.go:223 +0x965
github.com/linuxkit/linuxkit/pkg/metadata/vendor/github.com/diskfs/go-diskfs/disk.(*Disk).GetFilesystem(0xc000078230, 0x0, 0x0, 0xc000078230, 0x0, 0x0)
	/go/src/github.com/linuxkit/linuxkit/pkg/metadata/vendor/github.com/diskfs/go-diskfs/disk/disk.go:227 +0x241

when i mount the cdrom in linux it works fine and i can list the files on it and view them

here is an image of the cdrom: broken-cdrom.img.gz

Creating an FAT32 block file

I am trying to create a FAT32 block file with files in it.
Basicly I want to achive the same as:

dd if=/dev/zero of=test.raw bs=100M count=10
mount test.raw /tmp/fs/
echo Hello World > /tmp/fs/test
umount /tmp/fs/

I made my way upto creating the file but I was unable to write some data on it.
(The functions succeed but after mounting the file nothing is on the partition)

Any advice?

package main

import (
    "os"
    "testing"

    "github.com/deitch/diskfs/disk"
    "github.com/deitch/diskfs/filesystem"
)

func TestFat32(t *testing.T) {
    f, err := os.Create("test.raw")
    if err != nil {
        panic(err)
    }
    // make it a 10MB file
    f.Truncate(10 * 1024 * 1024)

    fileInfo, err := f.Stat()
    if err != nil {
        t.Fatalf("Error reading info on temporary disk: %v", err)
    }

    d := &disk.Disk{
        File: f,
        LogicalBlocksize: 512,
        PhysicalBlocksize: 512,
        Info: fileInfo,
        Size: fileInfo.Size(),
    }
    // // this is partition start and end in sectors, not bytes
    // sectorSize := 512
    // partitionStart := uint64(2048)
    // // make it a 5MB partition
    // partitionSize := uint64(5 * 1024 * 1024 / sectorSize)
    // partitionEnd := partitionSize + partitionStart - 1

    // // apply the partition table
    // err = d.Partition(&gpt.Table{
    //  Partitions: []*gpt.Partition{
    //      {
    //          Start: partitionStart,
    //          End: partitionEnd,
    //          Type: gpt.EFISystemPartition,
    //          Name: "Disk",
    //      },
    //  },
    //  LogicalSectorSize: sectorSize,
    // })
    // if err != nil {
    //  panic(err)
    // }

    fs, err := d.CreateFilesystem(0, filesystem.TypeFat32)
    if err != nil {
        panic(err)
    }
    rw, err := fs.OpenFile("/test", os.O_CREATE|os.O_RDWR)
    if err != nil {
        panic(err)
    }

    _, err = rw.Write([]byte("Hello World"))
    if err != nil {
        panic(err)
    }
}

ntfs support

github.com/Velocidex/go-ntfs
github.com/gentlemanautomaton/ntfs

Thank you

Question: Can this be used to flash images to disks?

Hi,

Sorry if this is the wrong place to ask, but I have been trying to find a Go library that can flash ISO and IMG images. I am essentially looking for a dd alternative that can flash various Linux and BSD images. This project seems like it might be able to do that but it seems a little complicated so I am not sure if I understand it correctly.

If it can't do that currently, is there any plan on adding that functionality? Are you aware of any other libraries that do that?

FAT32 io.Copy performance

Hello there.

I am evaluating go-diskfs (nice project!) for use with our tamago framework to port interlock to bare metal. The integration is promising however I am stumbling upon some issues.

First of all while using io.Copy on a FAT32 file opened with OpenFile I notice that performance is pretty slow, it seems that write speed decreases as the file gets larger, I am using something similar to the following:

f, _ := storage.OpenFile(osPath, os.O_RDWR|os.O_CREATE|os.O_EXCL|os.O_TRUNC)
io.Copy(f, buf)

I think this relates to #110 (comment)

Additionally I notice that writes to the underlying FileSystem can be of sizes which are not a multiple of its block size, I am not sure if this is intended or not.

Despite this being correct or not, for sure it's problematic that WriteAt errors are not all trapped, for example my custom block device (which reads/writes from an SD card) does not allow writes which are not a multiple of block size, the WriteAt error was raised but it's never caught in fat32.allocateSpace (also not in some instances of fat32.Create).

So my issues are the following:

  • can write performance be improved ?
  • is it correct for WriteAt to be invoked without honoring the FileSystem block size ?
  • all WriteAt errors should be trapped.

Thanks!

lsblk does not show partitions after writing GPT partitions from docker container

What happened
Created a single partition on a 14GB disk using the below code snippet. The issue occurred when the partition was created from a docker container. When executed directly it worked as expected.

table := &gpt.Table{
		LogicalSectorSize: 512,
		ProtectiveMBR:     true,
	}
partition := &gpt.Partition{
		Start: 2048,
		End:   30277598,
		Type:  gpt.LinuxFilesystem,
	}
table.Partitions = append(table.Partitions, partition)
p := "/dev/sdb"
d,err := diskfs.Open(p)
err = d.Partition(table)

lsblk does not show the newly created partition /dev/sdb1.

sda          8:0    0 465.8G  0 disk 
├─sda1       8:1    0   487M  0 part /boot/efi
├─sda2       8:2    0     1K  0 part 
├─sda5       8:5    0 114.5G  0 part /
├─sda6       8:6    0 339.4G  0 part /home
└─sda7       8:7    0  11.5G  0 part [SWAP]
sdb          8:16   1  14.4G  0 disk 

But fdisk -l /dev/sdb shows that a partition is present.

Disk /dev/sdb: 14.4 GiB, 15502147584 bytes, 30277632 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: 4B33D1CC-0804-4A47-B237-D19CEA71F7E1

Device     Start      End  Sectors  Size Type
/dev/sdb1   2048 30277598 30275551 14.4G Linux filesystem

What I think the problem is?
Partition utilities like fdisk forces the kernel to reread the partition table after partition blocks are written, so the partition table gets updated immediately. If the table is not re-read, the time at which kernel probes the disk and updates cannot be determined.

Proposed solution
diskfs should use a ioctl(fd, BLKRRPART) call to force the kernel to re-read the partition table after the partition table is written onto the disk.
This needs to be done only in the case of blockdevices, while working with files this need not be the case.
Cons of the solution: It will make go-diskfs platform dependent as ioctl() is unix specific.

How to read squashfs files with an offset?

I am trying to read a squashfs file with an offset (the squashfs filesystem starts at byte 188392).

# Get a test file
wget -c https://github.com/AppImage/AppImageKit/releases/download/12/appimagetool-x86_64.AppImage

# Find out the offset of the squashfs image
$ chmod +x ./appimagetool-x86_64.AppImage
$ ./appimagetool-x86_64.AppImage --appimage-offset
188392

# Verify the offset 188392
$ dd if=appimagetool-x86_64.AppImage bs=1 skip=188392 count=16 | xxd -
00000000: 6873 7173 2800 0000 0000 0000 0000 0200  hsqs(...........
16+0 records in
16+0 records out

# Find out the block size of the squashfs image
$ unsquashfs -s -o 188392 appimagetool-x86_64.AppImage 
Found a valid SQUASHFS 4:0 superblock on appimagetool-x86_64.AppImage.
Creation or last append time Thu Jan  1 01:00:00 1970
Filesystem size 1785619 bytes (1743.77 Kbytes / 1.70 Mbytes)
Compression gzip
Block size 131072
Filesystem is exportable via NFS
Inodes are compressed
Data is compressed
Uids/Gids (Id table) are compressed
Fragments are compressed
Always-use-fragments option is not specified
Xattrs are compressed
Duplicates are removed
Number of fragments 3
Number of inodes 40
Number of ids 1

With this information, I am trying

package main

import (
	"log"
	"os"

	"github.com/diskfs/go-diskfs/filesystem/squashfs" // Have to use: GO111MODULE=on /usr/local/go/bin/go get github.com/diskfs/go-diskfs@squashfs
)

func main() {
	f, err := os.Open("/home/me/Downloads/appimagetool-x86_64.AppImage")
	if err != nil {
		log.Println(err)
	}
	defer f.Close()
	fs, err := squashfs.Read(f, 0, 188392, 131072)
	log.Println(fs.ReadDir("/"))
}

but I am getting

2019/11/02 09:43:53 error reading inode table: Error reading block at position 0: Read 543 instead of expected 20963 bytes for metadata block at location 0
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x6300d7]

What am I doing wrong?

Making two partitions in one table

Haven't found any instances of two partitions being made anywhere, is it supported? No matter what I do first partition just takes the rest of the disk.

(btw there's a typo in the filename, gpt/partiton.go)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.