// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build linux darwin dragonfly freebsd openbsd netbsd solaris

package tar

import (
	"io"
	"os"
	"runtime"
	"syscall"
)

func init() {
	sysSparseDetect = sparseDetectUnix
}

func sparseDetectUnix(f *os.File) (sph sparseHoles, err error) {
	// SEEK_DATA and SEEK_HOLE originated from Solaris and support for it
	// has been added to most of the other major Unix systems.
	var seekData, seekHole = 3, 4 // SEEK_DATA/SEEK_HOLE from unistd.h

	if runtime.GOOS == "darwin" {
		// Darwin has the constants swapped, compared to all other UNIX.
		seekData, seekHole = 4, 3
	}

	// Check for seekData/seekHole support.
	// Different OS and FS may differ in the exact errno that is returned when
	// there is no support. Rather than special-casing every possible errno
	// representing "not supported", just assume that a non-nil error means
	// that seekData/seekHole is not supported.
	if _, err := f.Seek(0, seekHole); err != nil {
		return nil, nil
	}

	// Populate the SparseHoles.
	var last, pos int64 = -1, 0
	for {
		// Get the location of the next hole section.
		if pos, err = fseek(f, pos, seekHole); pos == last || err != nil {
			return sph, err
		}
		offset := pos
		last = pos

		// Get the location of the next data section.
		if pos, err = fseek(f, pos, seekData); pos == last || err != nil {
			return sph, err
		}
		length := pos - offset
		last = pos

		if length > 0 {
			sph = append(sph, SparseEntry{offset, length})
		}
	}
}

func fseek(f *os.File, pos int64, whence int) (int64, error) {
	pos, err := f.Seek(pos, whence)
	if errno(err) == syscall.ENXIO {
		// SEEK_DATA returns ENXIO when past the last data fragment,
		// which makes determining the size of the last hole difficult.
		pos, err = f.Seek(0, io.SeekEnd)
	}
	return pos, err
}

func errno(err error) error {
	if perr, ok := err.(*os.PathError); ok {
		return perr.Err
	}
	return err
}