github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/pkg/ls/fileinfo_unix.go (about)

     1  // Copyright 2017 the u-root Authors. All rights reserved
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build !plan9
     6  // +build !plan9
     7  
     8  package ls
     9  
    10  import (
    11  	"fmt"
    12  	"os"
    13  	"os/user"
    14  	"regexp"
    15  	"strconv"
    16  	"strings"
    17  	"syscall"
    18  	"time"
    19  
    20  	humanize "github.com/dustin/go-humanize"
    21  	"golang.org/x/sys/unix"
    22  )
    23  
    24  // Matches characters which would interfere with ls's formatting.
    25  var unprintableRe = regexp.MustCompile("[[:cntrl:]\n]")
    26  
    27  // FileInfo holds file metadata.
    28  //
    29  // Since `os.FileInfo` is an interface, it is difficult to tweak some of its
    30  // internal values. For example, replacing the starting directory with a dot.
    31  // `extractImportantParts` populates our own struct which we can modify at will
    32  // before printing.
    33  type FileInfo struct {
    34  	Name          string
    35  	Mode          os.FileMode
    36  	Rdev          uint64
    37  	UID, GID      uint32
    38  	Size          int64
    39  	MTime         time.Time
    40  	SymlinkTarget string
    41  }
    42  
    43  // FromOSFileInfo converts os.FileInfo to an ls.FileInfo.
    44  func FromOSFileInfo(path string, fi os.FileInfo) FileInfo {
    45  	var link string
    46  
    47  	s := fi.Sys().(*syscall.Stat_t)
    48  	if fi.Mode()&os.ModeType == os.ModeSymlink {
    49  		if l, err := os.Readlink(path); err != nil {
    50  			link = err.Error()
    51  		} else {
    52  			link = l
    53  		}
    54  	}
    55  
    56  	return FileInfo{
    57  		Name:          fi.Name(),
    58  		Mode:          fi.Mode(),
    59  		Rdev:          uint64(s.Rdev),
    60  		UID:           s.Uid,
    61  		GID:           s.Gid,
    62  		Size:          fi.Size(),
    63  		MTime:         fi.ModTime(),
    64  		SymlinkTarget: link,
    65  	}
    66  }
    67  
    68  // PrintableName returns a printable file name.
    69  func (fi FileInfo) PrintableName() string {
    70  	return unprintableRe.ReplaceAllLiteralString(fi.Name, "?")
    71  }
    72  
    73  // Without this cache, `ls -l` is orders of magnitude slower.
    74  var (
    75  	uidCache = map[uint32]string{}
    76  	gidCache = map[uint32]string{}
    77  )
    78  
    79  // Convert uid to username, or return uid on error.
    80  func lookupUserName(id uint32) string {
    81  	if s, ok := uidCache[id]; ok {
    82  		return s
    83  	}
    84  	s := fmt.Sprint(id)
    85  	if u, err := user.LookupId(s); err == nil {
    86  		s = u.Username
    87  	}
    88  	uidCache[id] = s
    89  	return s
    90  }
    91  
    92  // Convert gid to group name, or return gid on error.
    93  func lookupGroupName(id uint32) string {
    94  	if s, ok := gidCache[id]; ok {
    95  		return s
    96  	}
    97  	s := fmt.Sprint(id)
    98  	if g, err := user.LookupGroupId(s); err == nil {
    99  		s = g.Name
   100  	}
   101  	gidCache[id] = s
   102  	return s
   103  }
   104  
   105  // Stringer provides a consistent way to format FileInfo.
   106  type Stringer interface {
   107  	// FileString formats a FileInfo.
   108  	FileString(fi FileInfo) string
   109  }
   110  
   111  // NameStringer is a Stringer implementation that just prints the name.
   112  type NameStringer struct{}
   113  
   114  // FileString implements Stringer.FileString and just returns fi's name.
   115  func (ns NameStringer) FileString(fi FileInfo) string {
   116  	return fi.PrintableName()
   117  }
   118  
   119  // QuotedStringer is a Stringer that returns the file name surrounded by qutoes
   120  // with escaped control characters.
   121  type QuotedStringer struct{}
   122  
   123  // FileString returns the name surrounded by quotes with escaped control characters.
   124  func (qs QuotedStringer) FileString(fi FileInfo) string {
   125  	return fmt.Sprintf("%#v", fi.Name)
   126  }
   127  
   128  // LongStringer is a Stringer that returns the file info formatted in `ls -l`
   129  // long format.
   130  type LongStringer struct {
   131  	Human bool
   132  	Name  Stringer
   133  }
   134  
   135  // FileString implements Stringer.FileString.
   136  func (ls LongStringer) FileString(fi FileInfo) string {
   137  	// Golang's FileMode.String() is almost sufficient, except we would
   138  	// rather use b and c for devices.
   139  	replacer := strings.NewReplacer("Dc", "c", "D", "b")
   140  
   141  	// Ex: crw-rw-rw-  root  root  1, 3  Feb 6 09:31  null
   142  	pattern := "%[1]s\t%[2]s\t%[3]s\t%[4]d, %[5]d\t%[7]v\t%[8]s"
   143  	if fi.Mode&os.ModeDevice == 0 && fi.Mode&os.ModeCharDevice == 0 {
   144  		// Ex: -rw-rw----  myuser  myuser  1256  Feb 6 09:31  recipes.txt
   145  		pattern = "%[1]s\t%[2]s\t%[3]s\t%[6]s\t%[7]v\t%[8]s"
   146  	}
   147  
   148  	var size string
   149  	if ls.Human {
   150  		size = humanize.Bytes(uint64(fi.Size))
   151  	} else {
   152  		size = strconv.FormatInt(fi.Size, 10)
   153  	}
   154  
   155  	s := fmt.Sprintf(pattern,
   156  		replacer.Replace(fi.Mode.String()),
   157  		lookupUserName(fi.UID),
   158  		lookupGroupName(fi.GID),
   159  		unix.Major(fi.Rdev),
   160  		unix.Minor(fi.Rdev),
   161  		size,
   162  		fi.MTime.Format("Jan _2 15:04"),
   163  		ls.Name.FileString(fi))
   164  
   165  	if fi.Mode&os.ModeType == os.ModeSymlink {
   166  		s += fmt.Sprintf(" -> %v", fi.SymlinkTarget)
   167  	}
   168  	return s
   169  }