| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701 |
- package m3u8
- /*
- Part of M3U8 parser & generator library.
- This file defines functions related to playlist parsing.
- Copyright 2013-2017 The Project Developers.
- See the AUTHORS and LICENSE files at the top-level directory of this distribution
- and at https://github.com/grafov/m3u8/
- ॐ तारे तुत्तारे तुरे स्व
- */
- import (
- "bytes"
- "errors"
- "fmt"
- "io"
- "regexp"
- "strconv"
- "strings"
- "time"
- )
- var reKeyValue = regexp.MustCompile(`([a-zA-Z0-9_-]+)=("[^"]+"|[^",]+)`)
- // Allow globally apply and/or override Time Parser function.
- // Available variants:
- // * FullTimeParse - implements full featured ISO/IEC 8601:2004
- // * StrictTimeParse - implements only RFC3339 Nanoseconds format
- var TimeParse func(value string) (time.Time, error) = FullTimeParse
- // Decode parses a master playlist passed from the buffer. If `strict`
- // parameter is true then it returns first syntax error.
- func (p *MasterPlaylist) Decode(data bytes.Buffer, strict bool) error {
- return p.decode(&data, strict)
- }
- // DecodeFrom parses a master playlist passed from the io.Reader
- // stream. If `strict` parameter is true then it returns first syntax
- // error.
- func (p *MasterPlaylist) DecodeFrom(reader io.Reader, strict bool) error {
- buf := new(bytes.Buffer)
- _, err := buf.ReadFrom(reader)
- if err != nil {
- return err
- }
- return p.decode(buf, strict)
- }
- // Parse master playlist. Internal function.
- func (p *MasterPlaylist) decode(buf *bytes.Buffer, strict bool) error {
- var eof bool
- state := new(decodingState)
- for !eof {
- line, err := buf.ReadString('\n')
- if err == io.EOF {
- eof = true
- } else if err != nil {
- break
- }
- err = decodeLineOfMasterPlaylist(p, state, line, strict)
- if strict && err != nil {
- return err
- }
- }
- if strict && !state.m3u {
- return errors.New("#EXTM3U absent")
- }
- return nil
- }
- // Decode parses a media playlist passed from the buffer. If `strict`
- // parameter is true then return first syntax error.
- func (p *MediaPlaylist) Decode(data bytes.Buffer, strict bool) error {
- return p.decode(&data, strict)
- }
- // DecodeFrom parses a media playlist passed from the io.Reader
- // stream. If `strict` parameter is true then it returns first syntax
- // error.
- func (p *MediaPlaylist) DecodeFrom(reader io.Reader, strict bool) error {
- buf := new(bytes.Buffer)
- _, err := buf.ReadFrom(reader)
- if err != nil {
- return err
- }
- return p.decode(buf, strict)
- }
- func (p *MediaPlaylist) decode(buf *bytes.Buffer, strict bool) error {
- var eof bool
- var line string
- var err error
- state := new(decodingState)
- wv := new(WV)
- for !eof {
- if line, err = buf.ReadString('\n'); err == io.EOF {
- eof = true
- } else if err != nil {
- break
- }
- err = decodeLineOfMediaPlaylist(p, wv, state, line, strict)
- if strict && err != nil {
- return err
- }
- }
- if state.tagWV {
- p.WV = wv
- }
- if strict && !state.m3u {
- return errors.New("#EXTM3U absent")
- }
- return nil
- }
- // Decode detects type of playlist and decodes it. It accepts bytes
- // buffer as input.
- func Decode(data bytes.Buffer, strict bool) (Playlist, ListType, error) {
- return decode(&data, strict)
- }
- // DecodeFrom detects type of playlist and decodes it. It accepts data
- // conformed with io.Reader.
- func DecodeFrom(reader io.Reader, strict bool) (Playlist, ListType, error) {
- buf := new(bytes.Buffer)
- _, err := buf.ReadFrom(reader)
- if err != nil {
- return nil, 0, err
- }
- return decode(buf, strict)
- }
- // Detect playlist type and decode it. May be used as decoder for both
- // master and media playlists.
- func decode(buf *bytes.Buffer, strict bool) (Playlist, ListType, error) {
- var eof bool
- var line string
- var master *MasterPlaylist
- var media *MediaPlaylist
- var listType ListType
- var err error
- state := new(decodingState)
- wv := new(WV)
- master = NewMasterPlaylist()
- media, err = NewMediaPlaylist(8, 1024) // Winsize for VoD will become 0, capacity auto extends
- if err != nil {
- return nil, 0, fmt.Errorf("Create media playlist failed: %s", err)
- }
- for !eof {
- if line, err = buf.ReadString('\n'); err == io.EOF {
- eof = true
- } else if err != nil {
- break
- }
- // fixes the issues https://github.com/grafov/m3u8/issues/25
- // TODO: the same should be done in decode functions of both Master- and MediaPlaylists
- // so some DRYing would be needed.
- if len(line) < 1 || line == "\r" {
- continue
- }
- err = decodeLineOfMasterPlaylist(master, state, line, strict)
- if strict && err != nil {
- return master, state.listType, err
- }
- err = decodeLineOfMediaPlaylist(media, wv, state, line, strict)
- if strict && err != nil {
- return media, state.listType, err
- }
- }
- if state.listType == MEDIA && state.tagWV {
- media.WV = wv
- }
- if strict && !state.m3u {
- return nil, listType, errors.New("#EXTM3U absent")
- }
- switch state.listType {
- case MASTER:
- return master, MASTER, nil
- case MEDIA:
- if media.Closed || media.MediaType == EVENT {
- // VoD and Event's should show the entire playlist
- media.SetWinSize(0)
- }
- return media, MEDIA, nil
- }
- return nil, state.listType, errors.New("Can't detect playlist type")
- }
- func decodeParamsLine(line string) map[string]string {
- out := make(map[string]string)
- for _, kv := range reKeyValue.FindAllStringSubmatch(line, -1) {
- k, v := kv[1], kv[2]
- out[k] = strings.Trim(v, ` "`)
- }
- return out
- }
- // Parse one line of master playlist.
- func decodeLineOfMasterPlaylist(p *MasterPlaylist, state *decodingState, line string, strict bool) error {
- var err error
- line = strings.TrimSpace(line)
- switch {
- case line == "#EXTM3U": // start tag first
- state.m3u = true
- case strings.HasPrefix(line, "#EXT-X-VERSION:"): // version tag
- state.listType = MASTER
- _, err = fmt.Sscanf(line, "#EXT-X-VERSION:%d", &p.ver)
- if strict && err != nil {
- return err
- }
- case strings.HasPrefix(line, "#EXT-X-MEDIA:"):
- var alt Alternative
- state.listType = MASTER
- for k, v := range decodeParamsLine(line[13:]) {
- switch k {
- case "TYPE":
- alt.Type = v
- case "GROUP-ID":
- alt.GroupId = v
- case "LANGUAGE":
- alt.Language = v
- case "NAME":
- alt.Name = v
- case "DEFAULT":
- if strings.ToUpper(v) == "YES" {
- alt.Default = true
- } else if strings.ToUpper(v) == "NO" {
- alt.Default = false
- } else if strict {
- return errors.New("value must be YES or NO")
- }
- case "AUTOSELECT":
- alt.Autoselect = v
- case "FORCED":
- alt.Forced = v
- case "CHARACTERISTICS":
- alt.Characteristics = v
- case "SUBTITLES":
- alt.Subtitles = v
- case "URI":
- alt.URI = v
- }
- }
- state.alternatives = append(state.alternatives, &alt)
- case !state.tagStreamInf && strings.HasPrefix(line, "#EXT-X-STREAM-INF:"):
- state.tagStreamInf = true
- state.listType = MASTER
- state.variant = new(Variant)
- if len(state.alternatives) > 0 {
- state.variant.Alternatives = state.alternatives
- state.alternatives = nil
- }
- p.Variants = append(p.Variants, state.variant)
- for k, v := range decodeParamsLine(line[18:]) {
- switch k {
- case "PROGRAM-ID":
- var val int
- val, err = strconv.Atoi(v)
- if strict && err != nil {
- return err
- }
- state.variant.ProgramId = uint32(val)
- case "BANDWIDTH":
- var val int
- val, err = strconv.Atoi(v)
- if strict && err != nil {
- return err
- }
- state.variant.Bandwidth = uint32(val)
- case "CODECS":
- state.variant.Codecs = v
- case "RESOLUTION":
- state.variant.Resolution = v
- case "AUDIO":
- state.variant.Audio = v
- case "VIDEO":
- state.variant.Video = v
- case "SUBTITLES":
- state.variant.Subtitles = v
- case "CLOSED-CAPTIONS":
- state.variant.Captions = v
- case "NAME":
- state.variant.Name = v
- }
- }
- case state.tagStreamInf && !strings.HasPrefix(line, "#"):
- state.tagStreamInf = false
- state.variant.URI = line
- case strings.HasPrefix(line, "#EXT-X-I-FRAME-STREAM-INF:"):
- state.listType = MASTER
- state.variant = new(Variant)
- state.variant.Iframe = true
- if len(state.alternatives) > 0 {
- state.variant.Alternatives = state.alternatives
- state.alternatives = nil
- }
- p.Variants = append(p.Variants, state.variant)
- for k, v := range decodeParamsLine(line[26:]) {
- switch k {
- case "URI":
- state.variant.URI = v
- case "PROGRAM-ID":
- var val int
- val, err = strconv.Atoi(v)
- if strict && err != nil {
- return err
- }
- state.variant.ProgramId = uint32(val)
- case "BANDWIDTH":
- var val int
- val, err = strconv.Atoi(v)
- if strict && err != nil {
- return err
- }
- state.variant.Bandwidth = uint32(val)
- case "CODECS":
- state.variant.Codecs = v
- case "RESOLUTION":
- state.variant.Resolution = v
- case "AUDIO":
- state.variant.Audio = v
- case "VIDEO":
- state.variant.Video = v
- }
- }
- case strings.HasPrefix(line, "#"): // unknown tags treated as comments
- return err
- }
- return err
- }
- // Parse one line of media playlist.
- func decodeLineOfMediaPlaylist(p *MediaPlaylist, wv *WV, state *decodingState, line string, strict bool) error {
- var err error
- line = strings.TrimSpace(line)
- switch {
- case !state.tagInf && strings.HasPrefix(line, "#EXTINF:"):
- state.tagInf = true
- state.listType = MEDIA
- sepIndex := strings.Index(line, ",")
- if sepIndex == -1 {
- if strict {
- return fmt.Errorf("could not parse: %q", line)
- }
- sepIndex = len(line)
- }
- duration := line[8:sepIndex]
- if len(duration) > 0 {
- if state.duration, err = strconv.ParseFloat(duration, 64); strict && err != nil {
- return fmt.Errorf("Duration parsing error: %s", err)
- }
- }
- if len(line) > sepIndex {
- state.title = line[sepIndex+1:]
- }
- case !strings.HasPrefix(line, "#"):
- if state.tagInf {
- err := p.Append(line, state.duration, state.title)
- if err == ErrPlaylistFull {
- // Extend playlist by doubling size, reset internal state, try again.
- // If the second Append fails, the if err block will handle it.
- // Retrying instead of being recursive was chosen as the state maybe
- // modified non-idempotently.
- p.Segments = append(p.Segments, make([]*MediaSegment, p.Count())...)
- p.capacity = uint(len(p.Segments))
- p.tail = p.count
- err = p.Append(line, state.duration, state.title)
- }
- // Check err for first or subsequent Append()
- if err != nil {
- return err
- }
- state.tagInf = false
- }
- if state.tagRange {
- if err = p.SetRange(state.limit, state.offset); strict && err != nil {
- return err
- }
- state.tagRange = false
- }
- if state.tagSCTE35 {
- state.tagSCTE35 = false
- if err = p.SetSCTE35(state.scte); strict && err != nil {
- return err
- }
- }
- if state.tagDiscontinuity {
- state.tagDiscontinuity = false
- if err = p.SetDiscontinuity(); strict && err != nil {
- return err
- }
- }
- if state.tagProgramDateTime {
- state.tagProgramDateTime = false
- if err = p.SetProgramDateTime(state.programDateTime); strict && err != nil {
- return err
- }
- }
- // If EXT-X-KEY appeared before reference to segment (EXTINF) then it linked to this segment
- if state.tagKey {
- p.Segments[p.last()].Key = &Key{state.xkey.Method, state.xkey.URI, state.xkey.IV, state.xkey.Keyformat, state.xkey.Keyformatversions}
- // First EXT-X-KEY may appeared in the header of the playlist and linked to first segment
- // but for convenient playlist generation it also linked as default playlist key
- if p.Key == nil {
- p.Key = state.xkey
- }
- state.tagKey = false
- }
- // If EXT-X-MAP appeared before reference to segment (EXTINF) then it linked to this segment
- if state.tagMap {
- p.Segments[p.last()].Map = &Map{state.xmap.URI, state.xmap.Limit, state.xmap.Offset}
- // First EXT-X-MAP may appeared in the header of the playlist and linked to first segment
- // but for convenient playlist generation it also linked as default playlist map
- if p.Map == nil {
- p.Map = state.xmap
- }
- state.tagMap = false
- }
- // start tag first
- case line == "#EXTM3U":
- state.m3u = true
- case line == "#EXT-X-ENDLIST":
- state.listType = MEDIA
- p.Closed = true
- case strings.HasPrefix(line, "#EXT-X-VERSION:"):
- state.listType = MEDIA
- if _, err = fmt.Sscanf(line, "#EXT-X-VERSION:%d", &p.ver); strict && err != nil {
- return err
- }
- case strings.HasPrefix(line, "#EXT-X-TARGETDURATION:"):
- state.listType = MEDIA
- if _, err = fmt.Sscanf(line, "#EXT-X-TARGETDURATION:%f", &p.TargetDuration); strict && err != nil {
- return err
- }
- case strings.HasPrefix(line, "#EXT-X-MEDIA-SEQUENCE:"):
- state.listType = MEDIA
- if _, err = fmt.Sscanf(line, "#EXT-X-MEDIA-SEQUENCE:%d", &p.SeqNo); strict && err != nil {
- return err
- }
- case strings.HasPrefix(line, "#EXT-X-PLAYLIST-TYPE:"):
- state.listType = MEDIA
- var playlistType string
- _, err = fmt.Sscanf(line, "#EXT-X-PLAYLIST-TYPE:%s", &playlistType)
- if err != nil {
- if strict {
- return err
- }
- } else {
- switch playlistType {
- case "EVENT":
- p.MediaType = EVENT
- case "VOD":
- p.MediaType = VOD
- }
- }
- case strings.HasPrefix(line, "#EXT-X-KEY:"):
- state.listType = MEDIA
- state.xkey = new(Key)
- for k, v := range decodeParamsLine(line[11:]) {
- switch k {
- case "METHOD":
- state.xkey.Method = v
- case "URI":
- state.xkey.URI = v
- case "IV":
- state.xkey.IV = v
- case "KEYFORMAT":
- state.xkey.Keyformat = v
- case "KEYFORMATVERSIONS":
- state.xkey.Keyformatversions = v
- }
- }
- state.tagKey = true
- case strings.HasPrefix(line, "#EXT-X-MAP:"):
- state.listType = MEDIA
- state.xmap = new(Map)
- for k, v := range decodeParamsLine(line[11:]) {
- switch k {
- case "URI":
- state.xmap.URI = v
- case "BYTERANGE":
- if _, err = fmt.Sscanf(v, "%d@%d", &state.xmap.Limit, &state.xmap.Offset); strict && err != nil {
- return fmt.Errorf("Byterange sub-range length value parsing error: %s", err)
- }
- }
- }
- state.tagMap = true
- case !state.tagProgramDateTime && strings.HasPrefix(line, "#EXT-X-PROGRAM-DATE-TIME:"):
- state.tagProgramDateTime = true
- state.listType = MEDIA
- if state.programDateTime, err = TimeParse(line[25:]); strict && err != nil {
- return err
- }
- case !state.tagRange && strings.HasPrefix(line, "#EXT-X-BYTERANGE:"):
- state.tagRange = true
- state.listType = MEDIA
- state.offset = 0
- params := strings.SplitN(line[17:], "@", 2)
- if state.limit, err = strconv.ParseInt(params[0], 10, 64); strict && err != nil {
- return fmt.Errorf("Byterange sub-range length value parsing error: %s", err)
- }
- if len(params) > 1 {
- if state.offset, err = strconv.ParseInt(params[1], 10, 64); strict && err != nil {
- return fmt.Errorf("Byterange sub-range offset value parsing error: %s", err)
- }
- }
- case !state.tagSCTE35 && strings.HasPrefix(line, "#EXT-SCTE35:"):
- state.tagSCTE35 = true
- state.listType = MEDIA
- state.scte = new(SCTE)
- state.scte.Syntax = SCTE35_67_2014
- for attribute, value := range decodeParamsLine(line[12:]) {
- switch attribute {
- case "CUE":
- state.scte.Cue = value
- case "ID":
- state.scte.ID = value
- case "TIME":
- state.scte.Time, _ = strconv.ParseFloat(value, 64)
- }
- }
- case !state.tagSCTE35 && strings.HasPrefix(line, "#EXT-OATCLS-SCTE35:"):
- // EXT-OATCLS-SCTE35 contains the SCTE35 tag, EXT-X-CUE-OUT contains duration
- state.tagSCTE35 = true
- state.scte = new(SCTE)
- state.scte.Syntax = SCTE35_OATCLS
- state.scte.Cue = line[19:]
- case state.tagSCTE35 && state.scte.Syntax == SCTE35_OATCLS && strings.HasPrefix(line, "#EXT-X-CUE-OUT:"):
- // EXT-OATCLS-SCTE35 contains the SCTE35 tag, EXT-X-CUE-OUT contains duration
- state.scte.Time, _ = strconv.ParseFloat(line[15:], 64)
- state.scte.CueType = SCTE35Cue_Start
- case !state.tagSCTE35 && strings.HasPrefix(line, "#EXT-X-CUE-OUT-CONT:"):
- state.tagSCTE35 = true
- state.scte = new(SCTE)
- state.scte.Syntax = SCTE35_OATCLS
- state.scte.CueType = SCTE35Cue_Mid
- for attribute, value := range decodeParamsLine(line[20:]) {
- switch attribute {
- case "SCTE35":
- state.scte.Cue = value
- case "Duration":
- state.scte.Time, _ = strconv.ParseFloat(value, 64)
- case "ElapsedTime":
- state.scte.Elapsed, _ = strconv.ParseFloat(value, 64)
- }
- }
- case !state.tagSCTE35 && line == "#EXT-X-CUE-IN":
- state.tagSCTE35 = true
- state.scte = new(SCTE)
- state.scte.Syntax = SCTE35_OATCLS
- state.scte.CueType = SCTE35Cue_End
- case !state.tagDiscontinuity && strings.HasPrefix(line, "#EXT-X-DISCONTINUITY"):
- state.tagDiscontinuity = true
- state.listType = MEDIA
- case strings.HasPrefix(line, "#EXT-X-I-FRAMES-ONLY"):
- state.listType = MEDIA
- p.Iframe = true
- case strings.HasPrefix(line, "#WV-AUDIO-CHANNELS"):
- state.listType = MEDIA
- if _, err = fmt.Sscanf(line, "#WV-AUDIO-CHANNELS %d", &wv.AudioChannels); strict && err != nil {
- return err
- }
- if err == nil {
- state.tagWV = true
- }
- case strings.HasPrefix(line, "#WV-AUDIO-FORMAT"):
- state.listType = MEDIA
- if _, err = fmt.Sscanf(line, "#WV-AUDIO-FORMAT %d", &wv.AudioFormat); strict && err != nil {
- return err
- }
- if err == nil {
- state.tagWV = true
- }
- case strings.HasPrefix(line, "#WV-AUDIO-PROFILE-IDC"):
- state.listType = MEDIA
- if _, err = fmt.Sscanf(line, "#WV-AUDIO-PROFILE-IDC %d", &wv.AudioProfileIDC); strict && err != nil {
- return err
- }
- if err == nil {
- state.tagWV = true
- }
- case strings.HasPrefix(line, "#WV-AUDIO-SAMPLE-SIZE"):
- state.listType = MEDIA
- if _, err = fmt.Sscanf(line, "#WV-AUDIO-SAMPLE-SIZE %d", &wv.AudioSampleSize); strict && err != nil {
- return err
- }
- if err == nil {
- state.tagWV = true
- }
- case strings.HasPrefix(line, "#WV-AUDIO-SAMPLING-FREQUENCY"):
- state.listType = MEDIA
- if _, err = fmt.Sscanf(line, "#WV-AUDIO-SAMPLING-FREQUENCY %d", &wv.AudioSamplingFrequency); strict && err != nil {
- return err
- }
- if err == nil {
- state.tagWV = true
- }
- case strings.HasPrefix(line, "#WV-CYPHER-VERSION"):
- state.listType = MEDIA
- wv.CypherVersion = line[19:]
- state.tagWV = true
- case strings.HasPrefix(line, "#WV-ECM"):
- state.listType = MEDIA
- if _, err = fmt.Sscanf(line, "#WV-ECM %s", &wv.ECM); strict && err != nil {
- return err
- }
- if err == nil {
- state.tagWV = true
- }
- case strings.HasPrefix(line, "#WV-VIDEO-FORMAT"):
- state.listType = MEDIA
- if _, err = fmt.Sscanf(line, "#WV-VIDEO-FORMAT %d", &wv.VideoFormat); strict && err != nil {
- return err
- }
- if err == nil {
- state.tagWV = true
- }
- case strings.HasPrefix(line, "#WV-VIDEO-FRAME-RATE"):
- state.listType = MEDIA
- if _, err = fmt.Sscanf(line, "#WV-VIDEO-FRAME-RATE %d", &wv.VideoFrameRate); strict && err != nil {
- return err
- }
- if err == nil {
- state.tagWV = true
- }
- case strings.HasPrefix(line, "#WV-VIDEO-LEVEL-IDC"):
- state.listType = MEDIA
- if _, err = fmt.Sscanf(line, "#WV-VIDEO-LEVEL-IDC %d", &wv.VideoLevelIDC); strict && err != nil {
- return err
- }
- if err == nil {
- state.tagWV = true
- }
- case strings.HasPrefix(line, "#WV-VIDEO-PROFILE-IDC"):
- state.listType = MEDIA
- if _, err = fmt.Sscanf(line, "#WV-VIDEO-PROFILE-IDC %d", &wv.VideoProfileIDC); strict && err != nil {
- return err
- }
- if err == nil {
- state.tagWV = true
- }
- case strings.HasPrefix(line, "#WV-VIDEO-RESOLUTION"):
- state.listType = MEDIA
- wv.VideoResolution = line[21:]
- state.tagWV = true
- case strings.HasPrefix(line, "#WV-VIDEO-SAR"):
- state.listType = MEDIA
- if _, err = fmt.Sscanf(line, "#WV-VIDEO-SAR %s", &wv.VideoSAR); strict && err != nil {
- return err
- }
- if err == nil {
- state.tagWV = true
- }
- case strings.HasPrefix(line, "#"): // unknown tags treated as comments
- return err
- }
- return err
- }
- // StrictTimeParse implements RFC3339 with Nanoseconds accuracy.
- func StrictTimeParse(value string) (time.Time, error) {
- return time.Parse(DATETIME, value)
- }
- // FullTimeParse implements ISO/IEC 8601:2004.
- func FullTimeParse(value string) (time.Time, error) {
- layouts := []string{
- "2006-01-02T15:04:05.999999999Z0700",
- "2006-01-02T15:04:05.999999999Z07:00",
- "2006-01-02T15:04:05.999999999Z07",
- }
- var (
- err error
- t time.Time
- )
- for _, layout := range layouts {
- if t, err = time.Parse(layout, value); err == nil {
- return t, nil
- }
- }
- return t, err
- }
|