@@ -85,12 +85,8 @@ func Content(input string) string {
8585
8686const maxContentFilterPasses = 4
8787
88- // maxLinkDestinationLength bounds how far destination-scanning functions look
89- // for a terminator (matching bracket, closing angle bracket) before giving
90- // up. Without a bound, an unterminated destination forces a scan to the end
91- // of input on every call site, which is quadratic across many such
92- // candidates.
9388const maxLinkDestinationLength = 4096
89+ const maxLinkLabelLength = 999
9490
9591var markdownParser = goldmark .DefaultParser ()
9692
@@ -511,11 +507,11 @@ func referenceDestinationSpan(source []byte, start int) (sourceSpan, int, bool)
511507 if offset >= len (source ) || source [offset ] != '[' || isBackslashEscapedBytes (source , offset ) {
512508 return sourceSpan {}, start , false
513509 }
514- closing := bytes . Index (source [ offset :], [] byte ( "]:" ) )
515- if closing < 0 {
510+ closing , ok := referenceLabelEnd (source , offset )
511+ if ! ok {
516512 return sourceSpan {}, start , false
517513 }
518- destinationStart := offset + closing + 2
514+ destinationStart := closing + 2
519515 for destinationStart < len (source ) && (source [destinationStart ] == ' ' || source [destinationStart ] == '\t' ) {
520516 destinationStart ++
521517 }
@@ -532,6 +528,39 @@ func referenceDestinationSpan(source []byte, start int) (sourceSpan, int, bool)
532528 return span , stop , true
533529}
534530
531+ func referenceLabelEnd (source []byte , start int ) (int , bool ) {
532+ depth := 1
533+ characters := 0
534+ for offset := start + 1 ; offset < len (source ) && source [offset ] != '\n' && source [offset ] != '\r' ; {
535+ if characters >= maxLinkLabelLength {
536+ return 0 , false
537+ }
538+ if source [offset ] == '\\' {
539+ _ , size := utf8 .DecodeRune (source [offset :])
540+ offset += size
541+ if offset < len (source ) && source [offset ] != '\n' && source [offset ] != '\r' {
542+ _ , size = utf8 .DecodeRune (source [offset :])
543+ offset += size
544+ characters ++
545+ }
546+ continue
547+ }
548+ switch source [offset ] {
549+ case '[' :
550+ depth ++
551+ case ']' :
552+ depth --
553+ if depth == 0 {
554+ return offset , offset + 1 < len (source ) && source [offset + 1 ] == ':'
555+ }
556+ }
557+ _ , size := utf8 .DecodeRune (source [offset :])
558+ offset += size
559+ characters ++
560+ }
561+ return 0 , false
562+ }
563+
535564// referenceLinkDestinationSpan parses a link reference definition's destination,
536565// which (unlike an inline link destination) is not wrapped in parentheses and
537566// instead terminates at whitespace, end of line, or an unescaped closing '>'
@@ -588,23 +617,25 @@ func angleAutoLinkSpan(source []byte, start int) (sourceSpan, int, bool) {
588617 if ! linkDestinationIsSafe (destination ) {
589618 return sourceSpan {}, start , false
590619 }
591- parsed , err := url .Parse (string (destination ))
592- if err != nil ||
593- (parsed .Scheme != "" &&
594- ! strings .EqualFold (parsed .Scheme , "http" ) &&
595- ! strings .EqualFold (parsed .Scheme , "https" ) &&
596- ! strings .EqualFold (parsed .Scheme , "mailto" )) {
620+ if ! isCommonMarkAutoLink (destination ) {
597621 return sourceSpan {}, start , false
598622 }
599623 return sourceSpan {start : start + 1 , stop : stop }, stop + 1 , true
600624}
601625
602626func inlineDestinationSpan (source []byte , start int ) (sourceSpan , int , bool ) {
627+ var lineBreaks int
603628 for start < len (source ) {
604- r , size := utf8 . DecodeRune (source [start :])
605- if ! unicode . IsSpace ( r ) {
629+ size , isLineBreak := markdownWhitespaceSize (source [start :])
630+ if size == 0 {
606631 break
607632 }
633+ if isLineBreak {
634+ lineBreaks ++
635+ if lineBreaks > 1 {
636+ return sourceSpan {}, start , false
637+ }
638+ }
608639 start += size
609640 }
610641 if start >= len (source ) {
@@ -616,8 +647,19 @@ func inlineDestinationSpan(source []byte, start int) (sourceSpan, int, bool) {
616647 for stop := start + 1 ; stop < limit ; stop ++ {
617648 if source [stop ] == '>' && ! isBackslashEscapedBytes (source , stop ) {
618649 closing := stop + 1
619- for closing < len (source ) && unicode .IsSpace (rune (source [closing ])) {
620- closing ++
650+ lineBreaks = 0
651+ for closing < len (source ) {
652+ size , isLineBreak := markdownWhitespaceSize (source [closing :])
653+ if size == 0 {
654+ break
655+ }
656+ if isLineBreak {
657+ lineBreaks ++
658+ if lineBreaks > 1 {
659+ return sourceSpan {}, start , false
660+ }
661+ }
662+ closing += size
621663 }
622664 if closing < len (source ) && source [closing ] == ')' {
623665 return sourceSpan {start : start + 1 , stop : stop }, closing + 1 , true
@@ -651,6 +693,82 @@ func inlineDestinationSpan(source []byte, start int) (sourceSpan, int, bool) {
651693 return sourceSpan {}, start , false
652694}
653695
696+ func markdownWhitespaceSize (source []byte ) (int , bool ) {
697+ switch source [0 ] {
698+ case ' ' , '\t' :
699+ return 1 , false
700+ case '\n' :
701+ return 1 , true
702+ case '\r' :
703+ if len (source ) > 1 && source [1 ] == '\n' {
704+ return 2 , true
705+ }
706+ return 1 , true
707+ default :
708+ return 0 , false
709+ }
710+ }
711+
712+ func isCommonMarkAutoLink (destination []byte ) bool {
713+ schemeEnd := bytes .IndexByte (destination , ':' )
714+ if schemeEnd > 0 {
715+ scheme := destination [:schemeEnd ]
716+ if bytes .EqualFold (scheme , []byte ("mailto" )) {
717+ return isMailtoAddress (destination [schemeEnd + 1 :])
718+ }
719+ if ! bytes .EqualFold (scheme , []byte ("http" )) && ! bytes .EqualFold (scheme , []byte ("https" )) {
720+ return false
721+ }
722+ parsed , err := url .Parse (string (destination ))
723+ return err == nil && parsed .Host != ""
724+ }
725+ at := bytes .IndexByte (destination , '@' )
726+ return at >= 1 && at + 1 < len (destination ) &&
727+ isCommonMarkEmail (destination [:at ], destination [at + 1 :])
728+ }
729+
730+ func isMailtoAddress (destination []byte ) bool {
731+ at := bytes .IndexByte (destination , '@' )
732+ return at > 0 && at + 1 < len (destination ) && bytes .IndexByte (destination [at + 1 :], '@' ) < 0 &&
733+ ! bytes .ContainsAny (destination , " \t \r \n <>" )
734+ }
735+
736+ func isCommonMarkEmail (local , domain []byte ) bool {
737+ if domain == nil {
738+ at := bytes .IndexByte (local , '@' )
739+ if at < 1 || at + 1 >= len (local ) {
740+ return false
741+ }
742+ domain = local [at + 1 :]
743+ local = local [:at ]
744+ }
745+ for _ , character := range local {
746+ if ! isEmailLocalCharacter (character ) {
747+ return false
748+ }
749+ }
750+ if ! isASCIIAlphaNumeric (domain [0 ]) {
751+ return false
752+ }
753+ for _ , character := range domain {
754+ switch {
755+ case isASCIIAlphaNumeric (character ), character == '.' , character == '-' :
756+ default :
757+ return false
758+ }
759+ }
760+ return domain [len (domain )- 1 ] != '.' && domain [len (domain )- 1 ] != '-'
761+ }
762+
763+ func isEmailLocalCharacter (character byte ) bool {
764+ return isASCIIAlphaNumeric (character ) || strings .ContainsRune ("._+-!#$%&'*+/=?^`{|}~" , rune (character ))
765+ }
766+
767+ func isASCIIAlphaNumeric (character byte ) bool {
768+ return (character >= 'a' && character <= 'z' ) || (character >= 'A' && character <= 'Z' ) ||
769+ (character >= '0' && character <= '9' )
770+ }
771+
654772func bareHTTPURLSpan (source []byte , start int ) (sourceSpan , bool ) {
655773 var prefixLength int
656774 switch {
0 commit comments