github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/s3select/sql/stringfuncs.go (about)

     1  // Copyright (c) 2015-2021 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package sql
    19  
    20  import (
    21  	"errors"
    22  	"strings"
    23  )
    24  
    25  var (
    26  	errMalformedEscapeSequence  = errors.New("Malformed escape sequence in LIKE clause")
    27  	errInvalidTrimArg           = errors.New("Trim argument is invalid - this should not happen")
    28  	errInvalidSubstringIndexLen = errors.New("Substring start index or length falls outside the string")
    29  )
    30  
    31  const (
    32  	percent    rune = '%'
    33  	underscore rune = '_'
    34  	runeZero   rune = 0
    35  )
    36  
    37  func evalSQLLike(text, pattern string, escape rune) (match bool, err error) {
    38  	s := []rune{}
    39  	prev := runeZero
    40  	hasLeadingPercent := false
    41  	patLen := len([]rune(pattern))
    42  	for i, r := range pattern {
    43  		if i > 0 && prev == escape {
    44  			switch r {
    45  			case percent, escape, underscore:
    46  				s = append(s, r)
    47  				prev = r
    48  				if r == escape {
    49  					prev = runeZero
    50  				}
    51  			default:
    52  				return false, errMalformedEscapeSequence
    53  			}
    54  			continue
    55  		}
    56  
    57  		prev = r
    58  
    59  		var ok bool
    60  		switch r {
    61  		case percent:
    62  			if len(s) == 0 {
    63  				hasLeadingPercent = true
    64  				continue
    65  			}
    66  
    67  			text, ok = matcher(text, string(s), hasLeadingPercent)
    68  			if !ok {
    69  				return false, nil
    70  			}
    71  			hasLeadingPercent = true
    72  			s = []rune{}
    73  
    74  			if i == patLen-1 {
    75  				// Last pattern character is a %, so
    76  				// we are done.
    77  				return true, nil
    78  			}
    79  
    80  		case underscore:
    81  			if len(s) == 0 {
    82  				text, ok = dropRune(text)
    83  				if !ok {
    84  					return false, nil
    85  				}
    86  				continue
    87  			}
    88  
    89  			text, ok = matcher(text, string(s), hasLeadingPercent)
    90  			if !ok {
    91  				return false, nil
    92  			}
    93  			hasLeadingPercent = false
    94  
    95  			text, ok = dropRune(text)
    96  			if !ok {
    97  				return false, nil
    98  			}
    99  			s = []rune{}
   100  
   101  		case escape:
   102  			if i == patLen-1 {
   103  				return false, errMalformedEscapeSequence
   104  			}
   105  			// Otherwise do nothing.
   106  
   107  		default:
   108  			s = append(s, r)
   109  		}
   110  
   111  	}
   112  	if hasLeadingPercent {
   113  		return strings.HasSuffix(text, string(s)), nil
   114  	}
   115  	return string(s) == text, nil
   116  }
   117  
   118  // matcher - Finds `pat` in `text`, and returns the part remainder of
   119  // `text`, after the match. If leadingPercent is false, `pat` must be
   120  // the prefix of `text`, otherwise it must be a substring.
   121  func matcher(text, pat string, leadingPercent bool) (res string, match bool) {
   122  	if !leadingPercent {
   123  		res = strings.TrimPrefix(text, pat)
   124  		if len(text) == len(res) {
   125  			return "", false
   126  		}
   127  	} else {
   128  		parts := strings.SplitN(text, pat, 2)
   129  		if len(parts) == 1 {
   130  			return "", false
   131  		}
   132  		res = parts[1]
   133  	}
   134  	return res, true
   135  }
   136  
   137  func dropRune(text string) (res string, ok bool) {
   138  	r := []rune(text)
   139  	if len(r) == 0 {
   140  		return "", false
   141  	}
   142  	return string(r[1:]), true
   143  }
   144  
   145  func evalSQLSubstring(s string, startIdx, length int) (res string, err error) {
   146  	rs := []rune(s)
   147  
   148  	// According to s3 document, if startIdx < 1, it is set to 1.
   149  	if startIdx < 1 {
   150  		startIdx = 1
   151  	}
   152  
   153  	if startIdx > len(rs) {
   154  		startIdx = len(rs) + 1
   155  	}
   156  
   157  	// StartIdx is 1-based in the input
   158  	startIdx--
   159  	endIdx := len(rs)
   160  	if length != -1 {
   161  		if length < 0 {
   162  			return "", errInvalidSubstringIndexLen
   163  		}
   164  
   165  		if length > (endIdx - startIdx) {
   166  			length = endIdx - startIdx
   167  		}
   168  
   169  		endIdx = startIdx + length
   170  	}
   171  
   172  	return string(rs[startIdx:endIdx]), nil
   173  }
   174  
   175  const (
   176  	trimLeading  = "LEADING"
   177  	trimTrailing = "TRAILING"
   178  	trimBoth     = "BOTH"
   179  )
   180  
   181  func evalSQLTrim(where *string, trimChars, text string) (result string, err error) {
   182  	cutSet := " "
   183  	if trimChars != "" {
   184  		cutSet = trimChars
   185  	}
   186  
   187  	trimFunc := strings.Trim
   188  	switch {
   189  	case where == nil:
   190  	case *where == trimBoth:
   191  	case *where == trimLeading:
   192  		trimFunc = strings.TrimLeft
   193  	case *where == trimTrailing:
   194  		trimFunc = strings.TrimRight
   195  	default:
   196  		return "", errInvalidTrimArg
   197  	}
   198  
   199  	return trimFunc(text, cutSet), nil
   200  }