pkg.re/essentialkaos/ek.v11@v12.41.0+incompatible/fsutil/fs.go (about)

     1  //go:build !windows
     2  // +build !windows
     3  
     4  // Package fsutil provides methods for working with files on POSIX compatible systems
     5  package fsutil
     6  
     7  // ////////////////////////////////////////////////////////////////////////////////// //
     8  //                                                                                    //
     9  //                         Copyright (c) 2022 ESSENTIAL KAOS                          //
    10  //      Apache License, Version 2.0 <https://www.apache.org/licenses/LICENSE-2.0>     //
    11  //                                                                                    //
    12  // ////////////////////////////////////////////////////////////////////////////////// //
    13  
    14  import (
    15  	"errors"
    16  	"fmt"
    17  	"os"
    18  	"strings"
    19  	"syscall"
    20  	"time"
    21  
    22  	PATH "pkg.re/essentialkaos/ek.v12/path"
    23  	"pkg.re/essentialkaos/ek.v12/system"
    24  )
    25  
    26  // ////////////////////////////////////////////////////////////////////////////////// //
    27  
    28  const (
    29  	_IFMT   = 0xF000
    30  	_IFSOCK = 0xC000
    31  	_IFREG  = 0x8000
    32  	_IFBLK  = 0x6000
    33  	_IFDIR  = 0x4000
    34  	_IFCHR  = 0x2000
    35  	_IRUSR  = 0x100
    36  	_IWUSR  = 0x80
    37  	_IXUSR  = 0x40
    38  	_IRGRP  = 0x20
    39  	_IWGRP  = 0x10
    40  	_IXGRP  = 0x8
    41  	_IROTH  = 0x4
    42  	_IWOTH  = 0x2
    43  	_IXOTH  = 0x1
    44  )
    45  
    46  // ////////////////////////////////////////////////////////////////////////////////// //
    47  
    48  // ErrEmptyPath can be returned by different methods if given path is empty and can't be
    49  // used
    50  var ErrEmptyPath = errors.New("Path is empty")
    51  
    52  // ////////////////////////////////////////////////////////////////////////////////// //
    53  
    54  // CheckPerms checks many props at once
    55  //
    56  //    * F - is file
    57  //    * D - is directory
    58  //    * X - is executable
    59  //    * L - is link
    60  //    * W - is writable
    61  //    * R - is readable
    62  //    * B - is block device
    63  //    * C - is character device
    64  //    * S - not empty (only for files)
    65  //
    66  func CheckPerms(props, path string) bool {
    67  	if props == "" || path == "" {
    68  		return false
    69  	}
    70  
    71  	path = PATH.Clean(path)
    72  	props = strings.ToUpper(props)
    73  
    74  	var stat = &syscall.Stat_t{}
    75  
    76  	err := syscall.Stat(path, stat)
    77  
    78  	if err != nil {
    79  		return false
    80  	}
    81  
    82  	user, err := getCurrentUser()
    83  
    84  	if err != nil {
    85  		return false
    86  	}
    87  
    88  	for _, k := range props {
    89  		switch k {
    90  
    91  		case 'F':
    92  			if stat.Mode&_IFMT != _IFREG {
    93  				return false
    94  			}
    95  
    96  		case 'D':
    97  			if stat.Mode&_IFMT != _IFDIR {
    98  				return false
    99  			}
   100  
   101  		case 'B':
   102  			if stat.Mode&_IFMT != _IFBLK {
   103  				return false
   104  			}
   105  
   106  		case 'C':
   107  			if stat.Mode&_IFMT != _IFCHR {
   108  				return false
   109  			}
   110  
   111  		case 'L':
   112  			if !IsLink(path) {
   113  				return false
   114  			}
   115  
   116  		case 'X':
   117  			if !isExecutableStat(stat, user.UID, getGIDList(user)) {
   118  				return false
   119  			}
   120  
   121  		case 'W':
   122  			if !isWritableStat(stat, user.UID, getGIDList(user)) {
   123  				return false
   124  			}
   125  
   126  		case 'R':
   127  			if !isReadableStat(stat, user.UID, getGIDList(user)) {
   128  				return false
   129  			}
   130  
   131  		case 'S':
   132  			if stat.Size == 0 {
   133  				return false
   134  			}
   135  		}
   136  	}
   137  
   138  	return true
   139  }
   140  
   141  // ValidatePerms validates permissions for file or directory
   142  func ValidatePerms(props, path string) error {
   143  	if props == "" || path == "" {
   144  		return errors.New("Props or path to object is empty")
   145  	}
   146  
   147  	path = PATH.Clean(path)
   148  	props = strings.ToUpper(props)
   149  
   150  	var stat = &syscall.Stat_t{}
   151  
   152  	err := syscall.Stat(path, stat)
   153  
   154  	if err != nil {
   155  		switch {
   156  		case strings.ContainsRune(props, 'F'):
   157  			return fmt.Errorf("File %s doesn't exist or not accessible", path)
   158  		case strings.ContainsRune(props, 'D'):
   159  			return fmt.Errorf("Directory %s doesn't exist or not accessible", path)
   160  		case strings.ContainsRune(props, 'B'):
   161  			return fmt.Errorf("Block device %s doesn't exist or not accessible", path)
   162  		case strings.ContainsRune(props, 'C'):
   163  			return fmt.Errorf("Character device %s doesn't exist or not accessible", path)
   164  		case strings.ContainsRune(props, 'L'):
   165  			return fmt.Errorf("Link %s doesn't exist or not accessible", path)
   166  		}
   167  
   168  		return fmt.Errorf("Object %s doesn't exist or not accessible", path)
   169  	}
   170  
   171  	user, err := getCurrentUser()
   172  
   173  	if err != nil {
   174  		return errors.New("Can't get information about the current user")
   175  	}
   176  
   177  	for _, k := range props {
   178  		switch k {
   179  
   180  		case 'F':
   181  			if stat.Mode&_IFMT != _IFREG {
   182  				return fmt.Errorf("%s is not a file", path)
   183  			}
   184  
   185  		case 'D':
   186  			if stat.Mode&_IFMT != _IFDIR {
   187  				return fmt.Errorf("%s is not a directory", path)
   188  			}
   189  
   190  		case 'B':
   191  			if stat.Mode&_IFMT != _IFBLK {
   192  				return fmt.Errorf("%s is not a block device", path)
   193  			}
   194  
   195  		case 'C':
   196  			if stat.Mode&_IFMT != _IFCHR {
   197  				return fmt.Errorf("%s is not a character device", path)
   198  			}
   199  
   200  		case 'L':
   201  			if !IsLink(path) {
   202  				return fmt.Errorf("%s is not a link", path)
   203  			}
   204  
   205  		case 'X':
   206  			if !isExecutableStat(stat, user.UID, getGIDList(user)) {
   207  				return fmt.Errorf("%s is not executable", path)
   208  			}
   209  
   210  		case 'W':
   211  			if !isWritableStat(stat, user.UID, getGIDList(user)) {
   212  				return fmt.Errorf("%s is not writable", path)
   213  			}
   214  
   215  		case 'R':
   216  			if !isReadableStat(stat, user.UID, getGIDList(user)) {
   217  				return fmt.Errorf("%s is not readable", path)
   218  			}
   219  
   220  		case 'S':
   221  			if stat.Size == 0 {
   222  				return fmt.Errorf("%s is empty", path)
   223  			}
   224  		}
   225  	}
   226  
   227  	return nil
   228  }
   229  
   230  // ProperPath returns the first proper path from a given slice
   231  func ProperPath(props string, paths []string) string {
   232  	for _, path := range paths {
   233  		path = PATH.Clean(path)
   234  
   235  		if CheckPerms(props, path) {
   236  			return path
   237  		}
   238  	}
   239  
   240  	return ""
   241  }
   242  
   243  // IsExist returns true if the given object is exist
   244  func IsExist(path string) bool {
   245  	if path == "" {
   246  		return false
   247  	}
   248  
   249  	path = PATH.Clean(path)
   250  
   251  	return syscall.Access(path, syscall.F_OK) == nil
   252  }
   253  
   254  // IsRegular returns true if the given object is a regular file
   255  func IsRegular(path string) bool {
   256  	if path == "" {
   257  		return false
   258  	}
   259  
   260  	path = PATH.Clean(path)
   261  	mode := getMode(path)
   262  
   263  	if mode == 0 {
   264  		return false
   265  	}
   266  
   267  	return mode&_IFMT == _IFREG
   268  }
   269  
   270  // IsSocket returns true if the given object is a socket
   271  func IsSocket(path string) bool {
   272  	if path == "" {
   273  		return false
   274  	}
   275  
   276  	path = PATH.Clean(path)
   277  	mode := getMode(path)
   278  
   279  	if mode == 0 {
   280  		return false
   281  	}
   282  
   283  	return mode&_IFMT == _IFSOCK
   284  }
   285  
   286  // IsBlockDevice returns true if the given object is a device
   287  func IsBlockDevice(path string) bool {
   288  	if path == "" {
   289  		return false
   290  	}
   291  
   292  	path = PATH.Clean(path)
   293  	mode := getMode(path)
   294  
   295  	if mode == 0 {
   296  		return false
   297  	}
   298  
   299  	return mode&_IFMT == _IFBLK
   300  }
   301  
   302  // IsCharacterDevice returns true if the given object is a character device
   303  func IsCharacterDevice(path string) bool {
   304  	if path == "" {
   305  		return false
   306  	}
   307  
   308  	path = PATH.Clean(path)
   309  	mode := getMode(path)
   310  
   311  	if mode == 0 {
   312  		return false
   313  	}
   314  
   315  	return mode&_IFMT == _IFCHR
   316  }
   317  
   318  // IsDir returns true if the given object is a directory
   319  func IsDir(path string) bool {
   320  	if path == "" {
   321  		return false
   322  	}
   323  
   324  	path = PATH.Clean(path)
   325  	mode := getMode(path)
   326  
   327  	if mode == 0 {
   328  		return false
   329  	}
   330  
   331  	return mode&_IFMT == _IFDIR
   332  }
   333  
   334  // IsLink returns true if the given object is a link
   335  func IsLink(path string) bool {
   336  	if path == "" {
   337  		return false
   338  	}
   339  
   340  	path = PATH.Clean(path)
   341  
   342  	var buf = make([]byte, 1)
   343  	_, err := syscall.Readlink(path, buf)
   344  
   345  	return err == nil
   346  }
   347  
   348  // IsReadable returns true if given object is readable by current user
   349  func IsReadable(path string) bool {
   350  	if path == "" {
   351  		return false
   352  	}
   353  
   354  	path = PATH.Clean(path)
   355  
   356  	var stat = &syscall.Stat_t{}
   357  
   358  	err := syscall.Stat(path, stat)
   359  
   360  	if err != nil {
   361  		return false
   362  	}
   363  
   364  	user, err := getCurrentUser()
   365  
   366  	if err != nil {
   367  		return false
   368  	}
   369  
   370  	return isReadableStat(stat, user.UID, getGIDList(user))
   371  }
   372  
   373  // IsReadableByUser returns true if given object is readable by some user
   374  func IsReadableByUser(path, userName string) bool {
   375  	if path == "" {
   376  		return false
   377  	}
   378  
   379  	path = PATH.Clean(path)
   380  
   381  	var stat = &syscall.Stat_t{}
   382  
   383  	err := syscall.Stat(path, stat)
   384  
   385  	if err != nil {
   386  		return false
   387  	}
   388  
   389  	user, err := system.LookupUser(userName)
   390  
   391  	if err != nil {
   392  		return false
   393  	}
   394  
   395  	return isReadableStat(stat, user.UID, getGIDList(user))
   396  }
   397  
   398  // IsWritable returns true if given object is writable by current user
   399  func IsWritable(path string) bool {
   400  	if path == "" {
   401  		return false
   402  	}
   403  
   404  	path = PATH.Clean(path)
   405  
   406  	var stat = &syscall.Stat_t{}
   407  
   408  	err := syscall.Stat(path, stat)
   409  
   410  	if err != nil {
   411  		return false
   412  	}
   413  
   414  	user, err := getCurrentUser()
   415  
   416  	if err != nil {
   417  		return false
   418  	}
   419  
   420  	return isWritableStat(stat, user.UID, getGIDList(user))
   421  }
   422  
   423  // IsWritableByUser returns true if given object is writable by some user
   424  func IsWritableByUser(path, userName string) bool {
   425  	if path == "" {
   426  		return false
   427  	}
   428  
   429  	path = PATH.Clean(path)
   430  
   431  	var stat = &syscall.Stat_t{}
   432  
   433  	err := syscall.Stat(path, stat)
   434  
   435  	if err != nil {
   436  		return false
   437  	}
   438  
   439  	user, err := system.LookupUser(userName)
   440  
   441  	if err != nil {
   442  		return false
   443  	}
   444  
   445  	return isWritableStat(stat, user.UID, getGIDList(user))
   446  }
   447  
   448  // IsExecutable returns true if given object is executable by current user
   449  func IsExecutable(path string) bool {
   450  	if path == "" {
   451  		return false
   452  	}
   453  
   454  	path = PATH.Clean(path)
   455  
   456  	var stat = &syscall.Stat_t{}
   457  
   458  	err := syscall.Stat(path, stat)
   459  
   460  	if err != nil {
   461  		return false
   462  	}
   463  
   464  	user, err := getCurrentUser()
   465  
   466  	if err != nil {
   467  		return false
   468  	}
   469  
   470  	return isExecutableStat(stat, user.UID, getGIDList(user))
   471  }
   472  
   473  // IsExecutableByUser returns true if given object is executable by some user
   474  func IsExecutableByUser(path, userName string) bool {
   475  	if path == "" {
   476  		return false
   477  	}
   478  
   479  	path = PATH.Clean(path)
   480  
   481  	var stat = &syscall.Stat_t{}
   482  
   483  	err := syscall.Stat(path, stat)
   484  
   485  	if err != nil {
   486  		return false
   487  	}
   488  
   489  	user, err := system.LookupUser(userName)
   490  
   491  	if err != nil {
   492  		return false
   493  	}
   494  
   495  	return isExecutableStat(stat, user.UID, getGIDList(user))
   496  }
   497  
   498  // IsEmpty returns true if given file is empty
   499  func IsEmpty(path string) bool {
   500  	if path == "" {
   501  		return false
   502  	}
   503  
   504  	path = PATH.Clean(path)
   505  
   506  	return GetSize(path) == 0
   507  }
   508  
   509  // IsNonEmpty returns true if given file is not empty
   510  func IsNonEmpty(path string) bool {
   511  	if path == "" {
   512  		return false
   513  	}
   514  
   515  	path = PATH.Clean(path)
   516  
   517  	return GetSize(path) > 0
   518  }
   519  
   520  // IsEmptyDir returns true if given directory es empty
   521  func IsEmptyDir(path string) bool {
   522  	if path == "" {
   523  		return false
   524  	}
   525  
   526  	path = PATH.Clean(path)
   527  
   528  	fd, err := syscall.Open(path, syscall.O_RDONLY, 0)
   529  
   530  	if err != nil {
   531  		return false
   532  	}
   533  
   534  	defer syscall.Close(fd)
   535  
   536  	n, err := syscall.ReadDirent(fd, make([]byte, 4096))
   537  
   538  	if isEmptyDirent(n) || err != nil {
   539  		return true
   540  	}
   541  
   542  	return false
   543  }
   544  
   545  // GetOwner returns object owner UID and GID
   546  func GetOwner(path string) (int, int, error) {
   547  	if path == "" {
   548  		return -1, -1, ErrEmptyPath
   549  	}
   550  
   551  	path = PATH.Clean(path)
   552  
   553  	var stat = &syscall.Stat_t{}
   554  
   555  	err := syscall.Stat(path, stat)
   556  
   557  	if err != nil {
   558  		return -1, -1, err
   559  	}
   560  
   561  	return int(stat.Uid), int(stat.Gid), nil
   562  }
   563  
   564  // GetATime returns time of last access
   565  func GetATime(path string) (time.Time, error) {
   566  	if path == "" {
   567  		return time.Time{}, ErrEmptyPath
   568  	}
   569  
   570  	path = PATH.Clean(path)
   571  
   572  	atime, _, _, err := GetTimes(path)
   573  
   574  	return atime, err
   575  }
   576  
   577  // GetCTime returns time of creation
   578  func GetCTime(path string) (time.Time, error) {
   579  	if path == "" {
   580  		return time.Time{}, ErrEmptyPath
   581  	}
   582  
   583  	path = PATH.Clean(path)
   584  
   585  	_, _, ctime, err := GetTimes(path)
   586  
   587  	return ctime, err
   588  }
   589  
   590  // GetMTime returns time of modification
   591  func GetMTime(path string) (time.Time, error) {
   592  	if path == "" {
   593  		return time.Time{}, ErrEmptyPath
   594  	}
   595  
   596  	path = PATH.Clean(path)
   597  
   598  	_, mtime, _, err := GetTimes(path)
   599  
   600  	return mtime, err
   601  }
   602  
   603  // GetSize returns file size in bytes
   604  func GetSize(path string) int64 {
   605  	if path == "" {
   606  		return -1
   607  	}
   608  
   609  	path = PATH.Clean(path)
   610  
   611  	var stat = &syscall.Stat_t{}
   612  
   613  	err := syscall.Stat(path, stat)
   614  
   615  	if err != nil {
   616  		return -1
   617  	}
   618  
   619  	return stat.Size
   620  }
   621  
   622  // GetMode returns file mode bits
   623  func GetMode(path string) os.FileMode {
   624  	if path == "" {
   625  		return 0
   626  	}
   627  
   628  	path = PATH.Clean(path)
   629  
   630  	return os.FileMode(getMode(path) & 0777)
   631  }
   632  
   633  // ////////////////////////////////////////////////////////////////////////////////// //
   634  
   635  func getMode(path string) uint32 {
   636  	var stat = &syscall.Stat_t{}
   637  
   638  	err := syscall.Stat(path, stat)
   639  
   640  	if err != nil {
   641  		return 0
   642  	}
   643  
   644  	return uint32(stat.Mode)
   645  }
   646  
   647  func isReadableStat(stat *syscall.Stat_t, uid int, gids []int) bool {
   648  	if uid == 0 {
   649  		return true
   650  	}
   651  
   652  	if stat.Mode&_IROTH == _IROTH {
   653  		return true
   654  	}
   655  
   656  	if stat.Mode&_IRUSR == _IRUSR && uid == int(stat.Uid) {
   657  		return true
   658  	}
   659  
   660  	for _, gid := range gids {
   661  		if stat.Mode&_IRGRP == _IRGRP && gid == int(stat.Gid) {
   662  			return true
   663  		}
   664  	}
   665  
   666  	return false
   667  }
   668  
   669  func isWritableStat(stat *syscall.Stat_t, uid int, gids []int) bool {
   670  	if uid == 0 {
   671  		return true
   672  	}
   673  
   674  	if stat.Mode&_IWOTH == _IWOTH {
   675  		return true
   676  	}
   677  
   678  	if stat.Mode&_IWUSR == _IWUSR && uid == int(stat.Uid) {
   679  		return true
   680  	}
   681  
   682  	for _, gid := range gids {
   683  		if stat.Mode&_IWGRP == _IWGRP && gid == int(stat.Gid) {
   684  			return true
   685  		}
   686  	}
   687  
   688  	return false
   689  }
   690  
   691  func isExecutableStat(stat *syscall.Stat_t, uid int, gids []int) bool {
   692  	if uid == 0 {
   693  		return true
   694  	}
   695  
   696  	if stat.Mode&_IXOTH == _IXOTH {
   697  		return true
   698  	}
   699  
   700  	if stat.Mode&_IXUSR == _IXUSR && uid == int(stat.Uid) {
   701  		return true
   702  	}
   703  
   704  	for _, gid := range gids {
   705  		if stat.Mode&_IXGRP == _IXGRP && gid == int(stat.Gid) {
   706  			return true
   707  		}
   708  	}
   709  
   710  	return false
   711  }
   712  
   713  func getGIDList(user *system.User) []int {
   714  	if user == nil {
   715  		return nil
   716  	}
   717  
   718  	var result []int
   719  
   720  	for _, group := range user.Groups {
   721  		result = append(result, group.GID)
   722  	}
   723  
   724  	return result
   725  }