// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

// +build linux

package cgroups

import (
	"bufio"
	"os"
	"path/filepath"
	"strconv"
	"strings"
)

const (
	_mountInfoSep               = " "
	_mountInfoOptsSep           = ","
	_mountInfoOptionalFieldsSep = "-"
)

const (
	_miFieldIDMountID = iota
	_miFieldIDParentID
	_miFieldIDDeviceID
	_miFieldIDRoot
	_miFieldIDMountPoint
	_miFieldIDOptions
	_miFieldIDOptionalFields

	_miFieldCountFirstHalf
)

const (
	_miFieldOffsetFSType = iota
	_miFieldOffsetMountSource
	_miFieldOffsetSuperOptions

	_miFieldCountSecondHalf
)

const _miFieldCountMin = _miFieldCountFirstHalf + _miFieldCountSecondHalf

// MountPoint is the data structure for the mount points in
// `/proc/$PID/mountinfo`. See also proc(5) for more information.
type MountPoint struct {
	MountID        int
	ParentID       int
	DeviceID       string
	Root           string
	MountPoint     string
	Options        []string
	OptionalFields []string
	FSType         string
	MountSource    string
	SuperOptions   []string
}

// NewMountPointFromLine parses a line read from `/proc/$PID/mountinfo` and
// returns a new *MountPoint.
func NewMountPointFromLine(line string) (*MountPoint, error) {
	fields := strings.Split(line, _mountInfoSep)

	if len(fields) < _miFieldCountMin {
		return nil, mountPointFormatInvalidError{line}
	}

	mountID, err := strconv.Atoi(fields[_miFieldIDMountID])
	if err != nil {
		return nil, err
	}

	parentID, err := strconv.Atoi(fields[_miFieldIDParentID])
	if err != nil {
		return nil, err
	}

	for i, field := range fields[_miFieldIDOptionalFields:] {
		if field == _mountInfoOptionalFieldsSep {
			fsTypeStart := _miFieldIDOptionalFields + i + 1

			if len(fields) != fsTypeStart+_miFieldCountSecondHalf {
				return nil, mountPointFormatInvalidError{line}
			}

			miFieldIDFSType := _miFieldOffsetFSType + fsTypeStart
			miFieldIDMountSource := _miFieldOffsetMountSource + fsTypeStart
			miFieldIDSuperOptions := _miFieldOffsetSuperOptions + fsTypeStart

			return &MountPoint{
				MountID:        mountID,
				ParentID:       parentID,
				DeviceID:       fields[_miFieldIDDeviceID],
				Root:           fields[_miFieldIDRoot],
				MountPoint:     fields[_miFieldIDMountPoint],
				Options:        strings.Split(fields[_miFieldIDOptions], _mountInfoOptsSep),
				OptionalFields: fields[_miFieldIDOptionalFields:(fsTypeStart - 1)],
				FSType:         fields[miFieldIDFSType],
				MountSource:    fields[miFieldIDMountSource],
				SuperOptions:   strings.Split(fields[miFieldIDSuperOptions], _mountInfoOptsSep),
			}, nil
		}
	}

	return nil, mountPointFormatInvalidError{line}
}

// Translate converts an absolute path inside the *MountPoint's file system to
// the host file system path in the mount namespace the *MountPoint belongs to.
func (mp *MountPoint) Translate(absPath string) (string, error) {
	relPath, err := filepath.Rel(mp.Root, absPath)

	if err != nil {
		return "", err
	}
	if relPath == ".." || strings.HasPrefix(relPath, "../") {
		return "", pathNotExposedFromMountPointError{
			mountPoint: mp.MountPoint,
			root:       mp.Root,
			path:       absPath,
		}
	}

	return filepath.Join(mp.MountPoint, relPath), nil
}

// parseMountInfo parses procPathMountInfo (usually at `/proc/$PID/mountinfo`)
// and yields parsed *MountPoint into newMountPoint.
func parseMountInfo(procPathMountInfo string, newMountPoint func(*MountPoint) error) error {
	mountInfoFile, err := os.Open(procPathMountInfo)
	if err != nil {
		return err
	}
	defer mountInfoFile.Close()

	scanner := bufio.NewScanner(mountInfoFile)

	for scanner.Scan() {
		mountPoint, err := NewMountPointFromLine(scanner.Text())
		if err != nil {
			return err
		}
		if err := newMountPoint(mountPoint); err != nil {
			return err
		}
	}

	return scanner.Err()
}
