github.com/lestrrat-go/jwx/v2@v2.0.21/jwt/openid/birthdate.go (about)

     1  package openid
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"math"
     8  	"regexp"
     9  	"strconv"
    10  
    11  	"github.com/lestrrat-go/jwx/v2/internal/json"
    12  )
    13  
    14  // https://openid.net/specs/openid-connect-core-1_0.html
    15  //
    16  // End-User's birthday, represented as an ISO 8601:2004 [ISO8601‑2004] YYYY-MM-DD format.
    17  // The year MAY be 0000, indicating that it is omitted. To represent only the year, YYYY
    18  // format is allowed. Note that depending on the underlying platform's date related function,
    19  // providing just year can result in varying month and day, so the implementers need to
    20  // take this factor into account to correctly process the dates.
    21  
    22  type BirthdateClaim struct {
    23  	year  *int
    24  	month *int
    25  	day   *int
    26  }
    27  
    28  func (b BirthdateClaim) Year() int {
    29  	if b.year == nil {
    30  		return 0
    31  	}
    32  	return *(b.year)
    33  }
    34  
    35  func (b BirthdateClaim) Month() int {
    36  	if b.month == nil {
    37  		return 0
    38  	}
    39  	return *(b.month)
    40  }
    41  
    42  func (b BirthdateClaim) Day() int {
    43  	if b.day == nil {
    44  		return 0
    45  	}
    46  	return *(b.day)
    47  }
    48  
    49  func (b *BirthdateClaim) UnmarshalJSON(data []byte) error {
    50  	var s string
    51  	if err := json.Unmarshal(data, &s); err != nil {
    52  		return fmt.Errorf(`failed to unmarshal JSON string for birthdate claim: %w`, err)
    53  	}
    54  
    55  	if err := b.Accept(s); err != nil {
    56  		return fmt.Errorf(`failed to accept JSON value for birthdate claim: %w`, err)
    57  	}
    58  	return nil
    59  }
    60  
    61  var intSize int
    62  
    63  func init() {
    64  	switch math.MaxInt {
    65  	case math.MaxInt16:
    66  		intSize = 16
    67  	case math.MaxInt32:
    68  		intSize = 32
    69  	case math.MaxInt64:
    70  		intSize = 64
    71  	}
    72  }
    73  
    74  func parseBirthdayInt(s string) int {
    75  	i, _ := strconv.ParseInt(s, 10, intSize)
    76  	return int(i)
    77  }
    78  
    79  var birthdateRx = regexp.MustCompile(`^(\d{4})-(\d{2})-(\d{2})$`)
    80  
    81  // Accepts a value read from JSON, and converts it to a BirthdateClaim.
    82  // This method DOES NOT verify the correctness of a date.
    83  // Consumers should check for validity of dates such as Apr 31 et al
    84  func (b *BirthdateClaim) Accept(v interface{}) error {
    85  	b.year = nil
    86  	b.month = nil
    87  	b.day = nil
    88  	switch v := v.(type) {
    89  	case *BirthdateClaim:
    90  		if ptr := v.year; ptr != nil {
    91  			year := *ptr
    92  			b.year = &year
    93  		}
    94  		if ptr := v.month; ptr != nil {
    95  			month := *ptr
    96  			b.month = &month
    97  		}
    98  		if ptr := v.day; ptr != nil {
    99  			day := *ptr
   100  			b.day = &day
   101  		}
   102  		return nil
   103  	case string:
   104  		// yeah, regexp is slow. PR's welcome
   105  		indices := birthdateRx.FindStringSubmatchIndex(v)
   106  		if indices == nil {
   107  			return fmt.Errorf(`invalid pattern for birthdate`)
   108  		}
   109  		var tmp BirthdateClaim
   110  
   111  		// Okay, this really isn't kosher, but we're doing this for
   112  		// the coverage game... Because birthdateRx already checked that
   113  		// the string contains 3 strings with consecutive decimal values
   114  		// we can assume that strconv.ParseInt always succeeds.
   115  		// strconv.ParseInt (and strconv.ParseUint that it uses internally)
   116  		// only returns range errors, so we should be safe.
   117  		year := parseBirthdayInt(v[indices[2]:indices[3]])
   118  		if year <= 0 {
   119  			return fmt.Errorf(`failed to parse birthdate year`)
   120  		}
   121  		tmp.year = &year
   122  
   123  		month := parseBirthdayInt(v[indices[4]:indices[5]])
   124  		if month <= 0 {
   125  			return fmt.Errorf(`failed to parse birthdate month`)
   126  		}
   127  		tmp.month = &month
   128  
   129  		day := parseBirthdayInt(v[indices[6]:indices[7]])
   130  		if day <= 0 {
   131  			return fmt.Errorf(`failed to parse birthdate day`)
   132  		}
   133  		tmp.day = &day
   134  
   135  		*b = tmp
   136  		return nil
   137  	default:
   138  		return fmt.Errorf(`invalid type for birthdate: %T`, v)
   139  	}
   140  }
   141  
   142  func (b BirthdateClaim) encode(dst io.Writer) {
   143  	fmt.Fprintf(dst, "%04d-%02d-%02d", b.Year(), b.Month(), b.Day())
   144  }
   145  
   146  func (b BirthdateClaim) String() string {
   147  	var buf bytes.Buffer
   148  	b.encode(&buf)
   149  	return buf.String()
   150  }
   151  
   152  func (b BirthdateClaim) MarshalText() ([]byte, error) {
   153  	var buf bytes.Buffer
   154  	b.encode(&buf)
   155  	return buf.Bytes(), nil
   156  }