github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/cmn/cos/template.go (about)

     1  // Package cos provides common low-level types and utilities for all aistore projects
     2  /*
     3   * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved.
     4   */
     5  package cos
     6  
     7  import (
     8  	"bytes"
     9  	"errors"
    10  	"fmt"
    11  	"math"
    12  	"strconv"
    13  	"strings"
    14  	"unicode"
    15  )
    16  
    17  const (
    18  	WildcardMatchAll = "*"
    19  	EmptyMatchAll    = ""
    20  )
    21  
    22  func MatchAll(template string) bool { return template == EmptyMatchAll || template == WildcardMatchAll }
    23  
    24  // Supported syntax includes 3 standalone variations, 3 alternative formats:
    25  // 1. bash (or shell) brace expansion:
    26  //    * `prefix-{0..100}-suffix`
    27  //    * `prefix-{00001..00010..2}-gap-{001..100..2}-suffix`
    28  // 2. at style:
    29  //    * `prefix-@100-suffix`
    30  //    * `prefix-@00001-gap-@100-suffix`
    31  // 3. fmt style:
    32  //    * `prefix-%06d-suffix`
    33  // In all cases, prefix and/or suffix are optional.
    34  //
    35  // NOTE: if none of the above applies, `NewParsedTemplate()` simply returns
    36  //       `ParsedTemplate{Prefix = original template string}` with nil Ranges
    37  
    38  type (
    39  	TemplateRange struct {
    40  		Gap        string // characters after the range (to the next range or end of the string)
    41  		Start      int64
    42  		End        int64
    43  		Step       int64
    44  		DigitCount int
    45  	}
    46  	ParsedTemplate struct {
    47  		Prefix      string
    48  		Ranges      []TemplateRange
    49  		at          []int64
    50  		buf         bytes.Buffer
    51  		rangesCount int
    52  	}
    53  
    54  	errTemplateInvalid struct {
    55  		msg string
    56  	}
    57  )
    58  
    59  const (
    60  	invalidFmt      = "invalid 'fmt' template %q (expecting e.g. 'prefix-%%06d-suffix)"
    61  	invalidBash     = "invalid 'bash' template %q (expecting e.g. 'prefix-{0001..0010..1}-suffix')"
    62  	invalidAt       = "invalid 'at' template %q (expecting e.g. 'prefix-@00100-suffix')"
    63  	startAfterEnd   = "invalid '%s' template %q: 'start' cannot be greater than 'end'"
    64  	negativeStart   = "invalid '%s' template %q: 'start' is negative"
    65  	nonPositiveStep = "invalid '%s' template %q: 'step' is non-positive"
    66  )
    67  
    68  var (
    69  	ErrEmptyTemplate = errors.New("empty range template")
    70  
    71  	errTemplateNotBash = errors.New("not a 'bash' template")
    72  	errTemplateNotFmt  = errors.New("not an 'fmt' template")
    73  	errTemplateNotAt   = errors.New("not an 'at' template")
    74  )
    75  
    76  func newErrTemplateInvalid(efmt string, a ...any) error {
    77  	return &errTemplateInvalid{fmt.Sprintf(efmt, a...)}
    78  }
    79  func (e *errTemplateInvalid) Error() string { return e.msg }
    80  
    81  ////////////////////
    82  // ParsedTemplate //
    83  ////////////////////
    84  
    85  func NewParsedTemplate(template string) (parsed ParsedTemplate, err error) {
    86  	if MatchAll(template) {
    87  		err = ErrEmptyTemplate
    88  		return
    89  	}
    90  
    91  	parsed, err = ParseBashTemplate(template)
    92  	if err == nil || err != errTemplateNotBash {
    93  		return
    94  	}
    95  	parsed, err = ParseAtTemplate(template)
    96  	if err == nil || err != errTemplateNotAt {
    97  		return
    98  	}
    99  	parsed, err = ParseFmtTemplate(template)
   100  	if err == nil || err != errTemplateNotFmt {
   101  		return
   102  	}
   103  
   104  	// "pure" prefix w/ no ranges
   105  	return ParsedTemplate{Prefix: template}, nil
   106  }
   107  
   108  func (pt *ParsedTemplate) Clone() *ParsedTemplate {
   109  	if pt == nil {
   110  		return nil
   111  	}
   112  	clone := *pt
   113  	return &clone
   114  }
   115  
   116  func (pt *ParsedTemplate) Count() int64 {
   117  	count := int64(1)
   118  	for _, tr := range pt.Ranges {
   119  		step := (tr.End-tr.Start)/tr.Step + 1
   120  		count *= step
   121  	}
   122  	return count
   123  }
   124  
   125  // maxLen specifies maximum objects to be returned
   126  func (pt *ParsedTemplate) ToSlice(maxLen ...int) []string {
   127  	var i, n int
   128  	if len(maxLen) > 0 && maxLen[0] >= 0 {
   129  		n = maxLen[0]
   130  	} else {
   131  		n = int(pt.Count())
   132  	}
   133  	objs := make([]string, 0, n)
   134  	pt.InitIter()
   135  	for objName, hasNext := pt.Next(); hasNext && i < n; objName, hasNext = pt.Next() {
   136  		objs = append(objs, objName)
   137  		i++
   138  	}
   139  	return objs
   140  }
   141  
   142  func (pt *ParsedTemplate) InitIter() {
   143  	pt.rangesCount = len(pt.Ranges)
   144  	pt.at = make([]int64, pt.rangesCount)
   145  	for i, tr := range pt.Ranges {
   146  		pt.at[i] = tr.Start
   147  	}
   148  }
   149  
   150  func (pt *ParsedTemplate) Next() (string, bool) {
   151  	pt.buf.Reset()
   152  	for i := pt.rangesCount - 1; i >= 0; i-- {
   153  		if pt.at[i] > pt.Ranges[i].End {
   154  			if i == 0 {
   155  				return "", false
   156  			}
   157  			pt.at[i] = pt.Ranges[i].Start
   158  			pt.at[i-1] += pt.Ranges[i-1].Step
   159  		}
   160  	}
   161  	pt.buf.WriteString(pt.Prefix)
   162  	for i, tr := range pt.Ranges {
   163  		pt.buf.WriteString(fmt.Sprintf("%0*d%s", tr.DigitCount, pt.at[i], tr.Gap))
   164  	}
   165  	pt.at[pt.rangesCount-1] += pt.Ranges[pt.rangesCount-1].Step
   166  	return pt.buf.String(), true
   167  }
   168  
   169  //
   170  // parsing --- parsing --- parsing
   171  //
   172  
   173  // template: "prefix-%06d-suffix"
   174  // (both prefix and suffix are optional, here and elsewhere)
   175  func ParseFmtTemplate(template string) (pt ParsedTemplate, err error) {
   176  	percent := strings.IndexByte(template, '%')
   177  	if percent == -1 {
   178  		err = errTemplateNotFmt
   179  		return
   180  	}
   181  	if idx := strings.IndexByte(template[percent+1:], '%'); idx != -1 {
   182  		err = errTemplateNotFmt
   183  		return
   184  	}
   185  
   186  	d := strings.IndexByte(template[percent:], 'd')
   187  	if d == -1 {
   188  		err = newErrTemplateInvalid(invalidFmt, template)
   189  		return
   190  	}
   191  	d += percent
   192  
   193  	digitCount := 0
   194  	if d-percent > 1 {
   195  		s := template[percent+1 : d]
   196  		if len(s) == 1 {
   197  			err = newErrTemplateInvalid(invalidFmt, template)
   198  			return
   199  		}
   200  		if s[0] != '0' {
   201  			err = newErrTemplateInvalid(invalidFmt, template)
   202  			return
   203  		}
   204  		i, err := strconv.ParseInt(s[1:], 10, 64)
   205  		if err != nil {
   206  			return pt, newErrTemplateInvalid(invalidFmt, template)
   207  		} else if i < 0 {
   208  			return pt, newErrTemplateInvalid(invalidFmt, template)
   209  		}
   210  		digitCount = int(i)
   211  	}
   212  
   213  	return ParsedTemplate{
   214  		Prefix: template[:percent],
   215  		Ranges: []TemplateRange{{
   216  			Start:      0,
   217  			End:        math.MaxInt64 - 1,
   218  			Step:       1,
   219  			DigitCount: digitCount,
   220  			Gap:        template[d+1:],
   221  		}},
   222  	}, nil
   223  }
   224  
   225  // examples
   226  // - single-range: "prefix{0001..0010}suffix"
   227  // - multi-range:  "prefix-{00001..00010..2}-gap-{001..100..2}-suffix"
   228  // (both prefix and suffix are optional, here and elsewhere)
   229  func ParseBashTemplate(template string) (pt ParsedTemplate, err error) {
   230  	left := strings.IndexByte(template, '{')
   231  	if left == -1 {
   232  		err = errTemplateNotBash
   233  		return
   234  	}
   235  	right := strings.LastIndexByte(template, '}')
   236  	if right == -1 {
   237  		err = errTemplateNotBash
   238  		return
   239  	}
   240  	if right < left {
   241  		err = newErrTemplateInvalid(invalidBash, template)
   242  		return
   243  	}
   244  	pt.Prefix = template[:left]
   245  
   246  	for {
   247  		tr := TemplateRange{}
   248  
   249  		left := strings.IndexByte(template, '{')
   250  		if left == -1 {
   251  			break
   252  		}
   253  
   254  		right := strings.IndexByte(template, '}')
   255  		if right == -1 {
   256  			err = newErrTemplateInvalid(invalidBash, template)
   257  			return
   258  		}
   259  		if right < left {
   260  			err = newErrTemplateInvalid(invalidBash, template)
   261  			return
   262  		}
   263  		inside := template[left+1 : right]
   264  
   265  		numbers := strings.Split(inside, "..")
   266  		if len(numbers) < 2 || len(numbers) > 3 {
   267  			err = newErrTemplateInvalid(invalidBash, template)
   268  			return
   269  		} else if len(numbers) == 2 { // {0001..0999} case
   270  			if tr.Start, err = strconv.ParseInt(numbers[0], 10, 64); err != nil {
   271  				return
   272  			}
   273  			if tr.End, err = strconv.ParseInt(numbers[1], 10, 64); err != nil {
   274  				return
   275  			}
   276  			tr.Step = 1
   277  			tr.DigitCount = Min(len(numbers[0]), len(numbers[1]))
   278  		} else if len(numbers) == 3 { // {0001..0999..2} case
   279  			if tr.Start, err = strconv.ParseInt(numbers[0], 10, 64); err != nil {
   280  				return
   281  			}
   282  			if tr.End, err = strconv.ParseInt(numbers[1], 10, 64); err != nil {
   283  				return
   284  			}
   285  			if tr.Step, err = strconv.ParseInt(numbers[2], 10, 64); err != nil {
   286  				return
   287  			}
   288  			tr.DigitCount = Min(len(numbers[0]), len(numbers[1]))
   289  		}
   290  		if err = validateBoundaries("bash", template, tr.Start, tr.End, tr.Step); err != nil {
   291  			return
   292  		}
   293  
   294  		// apply gap (either to next range or end of the template)
   295  		template = template[right+1:]
   296  		right = strings.Index(template, "{")
   297  		if right >= 0 {
   298  			tr.Gap = template[:right]
   299  		} else {
   300  			tr.Gap = template
   301  		}
   302  
   303  		pt.Ranges = append(pt.Ranges, tr)
   304  	}
   305  	return
   306  }
   307  
   308  // e.g.:
   309  // - multi range:  "prefix-@00001-gap-@100-suffix"
   310  // - single range: "prefix@00100suffix"
   311  func ParseAtTemplate(template string) (pt ParsedTemplate, err error) {
   312  	left := strings.IndexByte(template, '@')
   313  	if left == -1 {
   314  		err = errTemplateNotAt
   315  		return
   316  	}
   317  	pt.Prefix = template[:left]
   318  
   319  	for {
   320  		tr := TemplateRange{}
   321  
   322  		left := strings.IndexByte(template, '@')
   323  		if left == -1 {
   324  			break
   325  		}
   326  
   327  		number := ""
   328  		for left++; len(template) > left && unicode.IsDigit(rune(template[left])); left++ {
   329  			number += string(template[left])
   330  		}
   331  
   332  		tr.Start = 0
   333  		if tr.End, err = strconv.ParseInt(number, 10, 64); err != nil {
   334  			return
   335  		}
   336  		tr.Step = 1
   337  		tr.DigitCount = len(number)
   338  
   339  		if err = validateBoundaries("at", template, tr.Start, tr.End, tr.Step); err != nil {
   340  			return
   341  		}
   342  
   343  		// apply gap (either to next range or end of the template)
   344  		template = template[left:]
   345  		right := strings.IndexByte(template, '@')
   346  		if right >= 0 {
   347  			tr.Gap = template[:right]
   348  		} else {
   349  			tr.Gap = template
   350  		}
   351  
   352  		pt.Ranges = append(pt.Ranges, tr)
   353  	}
   354  	return
   355  }
   356  
   357  func validateBoundaries(typ, template string, start, end, step int64) error {
   358  	if start > end {
   359  		return newErrTemplateInvalid(startAfterEnd, typ, template)
   360  	}
   361  	if start < 0 {
   362  		return newErrTemplateInvalid(negativeStart, typ, template)
   363  	}
   364  	if step <= 0 {
   365  		return newErrTemplateInvalid(nonPositiveStep, typ, template)
   366  	}
   367  	return nil
   368  }