Skip to content
This repository has been archived by the owner on Dec 8, 2024. It is now read-only.

Commit

Permalink
Global TimeParse function with custom parsers support
Browse files Browse the repository at this point in the history
  • Loading branch information
md2k committed Mar 3, 2017
1 parent cbf3011 commit 03d4770
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 43 deletions.
45 changes: 27 additions & 18 deletions reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ import (

var reKeyValue = regexp.MustCompile(`([a-zA-Z_-]+)=("[^"]+"|[^",]+)`)

// 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 {
Expand Down Expand Up @@ -502,7 +508,7 @@ func decodeLineOfMediaPlaylist(p *MediaPlaylist, wv *WV, state *decodingState, l
state.tagProgramDateTime = true
state.listType = MEDIA

if state.programDateTime, err = parseISO8601(line[25:]); strict && err != nil {
if state.programDateTime, err = TimeParse(line[25:]); strict && err != nil {
return err
}

Expand Down Expand Up @@ -641,23 +647,26 @@ func decodeLineOfMediaPlaylist(p *MediaPlaylist, wv *WV, state *decodingState, l
return err
}

// Custom DateTime Parser to implement support of ISO8601 Formats variants
func parseISO8601(dt string) (t time.Time, err error) {
switch {
case rgx_ISO8601.MatchString(dt):
if t, err = time.Parse(ISO8601, dt); err != nil {
return
}
case rgx_ISO8601Colon.MatchString(dt):
if t, err = time.Parse(ISO8601Colon, dt); err != nil {
return
}
case rgx_ISO8601Short.MatchString(dt):
if t, err = time.Parse(ISO8601Short, dt); err != nil {
return
// Strict Time Wrapper implements RFC3339 with Nanoseconds accuracy
func StrictTimeParse(value string) (time.Time, error) {
return time.Parse(DATETIME, value)
}

// Custom Time Parser 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
}
default:
err = fmt.Errorf("parsing time %s do not match ISO8601/RFC3339 patterns: %s .", dt, time.RFC3339Nano)
}
return
return t, err
}
103 changes: 94 additions & 9 deletions reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,9 @@ func TestDecodeMediaPlaylistAutoDetectExtend(t *testing.T) {
}

// Tests for Time Parsing of EXT-X-PROGRAM-DATE-TIME
// We testing RFC3339 where we ca nget time in UTC, UTC with Nanoseconds
// We testing ISO/IEC 8601:2004 where we can get time in UTC, UTC with Nanoseconds
// timeZone in formats '±00:00', '±0000', '±00'
// m3u8.FullTimeParse()
func TestTimeLayoutsDecode(t *testing.T) {
time_in_utc := "#EXT-X-PROGRAM-DATE-TIME:2006-01-02T15:04:05Z"
time_in_utc_nano := "#EXT-X-PROGRAM-DATE-TIME:2006-01-02T15:04:05.123456789Z"
Expand All @@ -319,56 +320,140 @@ func TestTimeLayoutsDecode(t *testing.T) {
var err error
err = decodeLineOfMediaPlaylist(p, wv, state, time_in_utc, true)
if err != nil {
t.Errorf("Time Parser Error for EXT-X-PROGRAM-DATE-TIME: %s", err)
t.Errorf("Time Parser Error for %s: %s", time_in_utc, err)
}

state = new(decodingState)
wv = new(WV)
err = decodeLineOfMediaPlaylist(p, wv, state, time_in_utc_nano, true)
if err != nil {
t.Errorf("Time Parser Error for EXT-X-PROGRAM-DATE-TIME: %s", err)
t.Errorf("Time Parser Error for %s: %s", time_in_utc_nano, err)
}

state = new(decodingState)
wv = new(WV)
err = decodeLineOfMediaPlaylist(p, wv, state, time_with_positive_zone_and_colon, true)
if err != nil {
t.Errorf("Time Parser Error for EXT-X-PROGRAM-DATE-TIME: %s", err)
t.Errorf("Time Parser Error for %s: %s", time_with_positive_zone_and_colon, err)
}

state = new(decodingState)
wv = new(WV)
err = decodeLineOfMediaPlaylist(p, wv, state, time_with_positive_zone_no_colon, true)
if err != nil {
t.Errorf("Time Parser Error for EXT-X-PROGRAM-DATE-TIME: %s", err)
t.Errorf("Time Parser Error for %s: %s", time_with_positive_zone_no_colon, err)
}

state = new(decodingState)
wv = new(WV)
err = decodeLineOfMediaPlaylist(p, wv, state, time_with_positive_zone_2digits, true)
if err != nil {
t.Errorf("Time Parser Error for EXT-X-PROGRAM-DATE-TIME: %s", err)
t.Errorf("Time Parser Error for %s: %s", time_with_positive_zone_2digits, err)
}

state = new(decodingState)
wv = new(WV)
err = decodeLineOfMediaPlaylist(p, wv, state, time_with_negative_zone_and_colon, true)
if err != nil {
t.Errorf("Time Parser Error for EXT-X-PROGRAM-DATE-TIME: %s", err)
t.Errorf("Time Parser Error for %s: %s", time_with_negative_zone_and_colon, err)
}

state = new(decodingState)
wv = new(WV)
err = decodeLineOfMediaPlaylist(p, wv, state, time_with_negative_zone_no_colon, true)
if err != nil {
t.Errorf("Time Parser Error for EXT-X-PROGRAM-DATE-TIME: %s", err)
t.Errorf("Time Parser Error for %s: %s", time_with_negative_zone_no_colon, err)
}

state = new(decodingState)
wv = new(WV)
err = decodeLineOfMediaPlaylist(p, wv, state, time_with_negative_zone_2digits, true)
if err != nil {
t.Errorf("Time Parser Error for EXT-X-PROGRAM-DATE-TIME: %s", err)
t.Errorf("Time Parser Error for %s: %s", time_with_negative_zone_2digits, err)
}
}

// Tests for Time Parsing of EXT-X-PROGRAM-DATE-TIME
// We testing Strict format of RFC3339 where we can get time in UTC, UTC with Nanoseconds
// timeZone in formats '±00:00', '±0000', '±00'
// m3u8.StrictTimeParse()
func TestStrictTimeLayoutsDecode(t *testing.T) {

// Should Pass
time_in_utc := "#EXT-X-PROGRAM-DATE-TIME:2006-01-02T15:04:05Z"
time_in_utc_nano := "#EXT-X-PROGRAM-DATE-TIME:2006-01-02T15:04:05.123456789Z"
time_with_positive_zone_and_colon := "#EXT-X-PROGRAM-DATE-TIME:2006-01-02T15:04:05+01:00"
time_with_negative_zone_and_colon := "#EXT-X-PROGRAM-DATE-TIME:2006-01-02T15:04:05-01:00"

// Should Fail
time_with_positive_zone_no_colon := "#EXT-X-PROGRAM-DATE-TIME:2006-01-02T15:04:05+0100"
time_with_positive_zone_2digits := "#EXT-X-PROGRAM-DATE-TIME:2006-01-02T15:04:05+01"
time_with_negative_zone_no_colon := "#EXT-X-PROGRAM-DATE-TIME:2006-01-02T15:04:05-0100"
time_with_negative_zone_2digits := "#EXT-X-PROGRAM-DATE-TIME:2006-01-02T15:04:05-01"

// Set TimeParse function to StrictTimeParse (RFC3339 Only)
TimeParse = StrictTimeParse

p, e := NewMediaPlaylist(1, 2)
if e != nil {
t.Fatalf("Create media playlist failed: %s", e)
}
state := new(decodingState)
wv := new(WV)

var err error
err = decodeLineOfMediaPlaylist(p, wv, state, time_in_utc, true)
if err != nil {
t.Errorf("Time Parser Error for %s: %s", time_in_utc, err)
}

state = new(decodingState)
wv = new(WV)
err = decodeLineOfMediaPlaylist(p, wv, state, time_in_utc_nano, true)
if err != nil {
t.Errorf("Time Parser Error for %s: %s", time_in_utc_nano, err)
}

state = new(decodingState)
wv = new(WV)
err = decodeLineOfMediaPlaylist(p, wv, state, time_with_positive_zone_and_colon, true)
if err != nil {
t.Errorf("Time Parser Error for %s: %s", time_with_positive_zone_and_colon, err)
}

state = new(decodingState)
wv = new(WV)
err = decodeLineOfMediaPlaylist(p, wv, state, time_with_positive_zone_no_colon, true)
if err != nil {
t.Logf("Time Parser Error for %s: %s", time_with_positive_zone_no_colon, err)
}

state = new(decodingState)
wv = new(WV)
err = decodeLineOfMediaPlaylist(p, wv, state, time_with_positive_zone_2digits, true)
if err != nil {
t.Logf("Time Parser Error for %s: %s", time_with_positive_zone_2digits, err)
}

state = new(decodingState)
wv = new(WV)
err = decodeLineOfMediaPlaylist(p, wv, state, time_with_negative_zone_and_colon, true)
if err != nil {
t.Errorf("Time Parser Error for %s: %s", time_with_negative_zone_and_colon, err)
}

state = new(decodingState)
wv = new(WV)
err = decodeLineOfMediaPlaylist(p, wv, state, time_with_negative_zone_no_colon, true)
if err != nil {
t.Logf("Time Parser Error for %s: %s", time_with_negative_zone_no_colon, err)
}

state = new(decodingState)
wv = new(WV)
err = decodeLineOfMediaPlaylist(p, wv, state, time_with_negative_zone_2digits, true)
if err != nil {
t.Logf("Time Parser Error for %s: %s", time_with_negative_zone_2digits, err)
}
}

Expand Down
16 changes: 0 additions & 16 deletions structure.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ package m3u8
import (
"bytes"
"io"
"regexp"
"time"
)

Expand All @@ -36,21 +35,6 @@ const (

DATETIME = time.RFC3339Nano // Format for EXT-X-PROGRAM-DATE-TIME defined in section 3.4.5

// time Layouts which is used during Manifest Ingestion for EXT-X-PROGRAM-DATE-TIME
// According to RFC3339 and ISO8601
// TimeZone can be set as '±00:00', '±0000', '±00' and all those variants is legit Datetime according to ISO8601/RFC3339
// So we want to have more precise Parser's layouts for incoming manifest to avoid time.Parse() error
// See this: https://en.wikipedia.org/wiki/ISO_8601
ISO8601 = "2006-01-02T15:04:05.999999999Z0700"
ISO8601Colon = "2006-01-02T15:04:05.999999999Z07:00"
ISO8601Short = "2006-01-02T15:04:05.999999999Z07"
)

var (
// Compile Regex to match incoming timeformat to use correct time layout for time.Parse()
rgx_ISO8601 = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.\d{1,9})?(Z|\+|\-)(\d{2}\d{2})?$`)
rgx_ISO8601Colon = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.\d{1,9})?(Z|\+|\-)(\d{2}:\d{2})?$`)
rgx_ISO8601Short = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.\d{1,9})?(Z|\+|\-)(\d{2})?$`)
)

type ListType uint
Expand Down

0 comments on commit 03d4770

Please sign in to comment.