@@ -473,50 +473,86 @@ defmodule String.Tokenizer do
473473 original_acc = :lists . reverse ( original_acc )
474474 acc = :unicode . characters_to_nfc_list ( original_acc )
475475
476- special =
476+ { different? , special } =
477477 if original_acc == acc do
478- special
478+ { false , special }
479479 else
480- [ :nfkc | List . delete ( special , :nfkc ) ]
480+ { true , [ :nfkc | List . delete ( special , :nfkc ) ] }
481481 end
482482
483- if scriptset != @ bottom or chunks_single? ( acc ) do
484- { kind , acc , rest , length , false , special }
485- else
486- breakdown =
487- for codepoint <- acc do
488- scriptsets =
489- case codepoint_to_scriptset ( codepoint ) do
490- @ top ->
491- ""
483+ cond do
484+ invalid_prefix = different? && invalid_normalized ( original_acc , acc , kind ) ->
485+ { :error , { :unexpected_token , invalid_prefix } }
492486
493- scriptset ->
494- scriptset
495- |> ScriptSet . to_indexes ( )
496- |> Enum . map ( & Map . fetch! ( @ indexed_scriptsets , & 1 ) )
497- |> then ( & ( " {" <> Enum . join ( & 1 , "," ) <> "}" ) )
498- end
487+ scriptset != @ bottom or chunks_single? ( acc ) ->
488+ { kind , acc , rest , length , false , special }
499489
500- hex = :io_lib . format ( ~c" ~4.16.0B" , [ codepoint ] )
501- " \\ u#{ hex } #{ [ codepoint ] } #{ scriptsets } \n "
502- end
490+ true ->
491+ breakdown =
492+ for codepoint <- acc do
493+ scriptsets =
494+ case codepoint_to_scriptset ( codepoint ) do
495+ @ top ->
496+ ""
497+
498+ scriptset ->
499+ scriptset
500+ |> ScriptSet . to_indexes ( )
501+ |> Enum . map ( & Map . fetch! ( @ indexed_scriptsets , & 1 ) )
502+ |> then ( & ( " {" <> Enum . join ( & 1 , "," ) <> "}" ) )
503+ end
504+
505+ hex = :io_lib . format ( ~c" ~4.16.0B" , [ codepoint ] )
506+ " \\ u#{ hex } #{ [ codepoint ] } #{ scriptsets } \n "
507+ end
503508
504- prefix = ~c" invalid mixed-script identifier found: "
509+ prefix = ~c" invalid mixed-script identifier found: "
505510
506- suffix = ~c"""
511+ suffix = ~c"""
507512
508513
509- Mixed-script identifiers are not supported for security reasons. \
510- '#{ acc } ' is made of the following scripts:\n
511- #{ breakdown }
512- Characters in identifiers from different scripts must be separated \
513- by underscore (_).
514- """
514+ Mixed-script identifiers are not supported for security reasons. \
515+ '#{ acc } ' is made of the following scripts:\n
516+ #{ breakdown }
517+ Characters in identifiers from different scripts must be separated \
518+ by underscore (_).
519+ """
515520
516- { :error , { :mixed_script , acc , { prefix , suffix } } }
521+ { :error , { :mixed_script , acc , { prefix , suffix } } }
517522 end
518523 end
519524
525+ defp invalid_normalized ( original_acc , acc , kind ) do
526+ case tokenize ( acc ) do
527+ { ^ kind , _acc , [ ] , _length , _ascii? , _special } ->
528+ false
529+
530+ { :error , { :unexpected_token , invalid_prefix } } ->
531+ invalid_original_prefix ( original_acc , invalid_prefix )
532+
533+ _other ->
534+ invalid_original_prefix ( original_acc , acc )
535+ end
536+ end
537+
538+ defp invalid_original_prefix ( original_acc , invalid_normalized_prefix ) do
539+ invalid_original_prefix ( original_acc , invalid_normalized_prefix , [ ] )
540+ end
541+
542+ defp invalid_original_prefix ( [ head | tail ] , invalid_normalized_prefix , prefix ) do
543+ prefix = [ head | prefix ]
544+
545+ if :unicode . characters_to_nfc_list ( :lists . reverse ( prefix ) ) == invalid_normalized_prefix do
546+ :lists . reverse ( prefix )
547+ else
548+ invalid_original_prefix ( tail , invalid_normalized_prefix , prefix )
549+ end
550+ end
551+
552+ defp invalid_original_prefix ( [ ] , _invalid_normalized_prefix , prefix ) do
553+ :lists . reverse ( prefix )
554+ end
555+
520556 # Support script mixing via chunked identifiers (UTS 55-5's strong recommends).
521557 # Each chunk in an ident like foo_bar_baz should pass checks.
522558 defp chunks_single? ( acc ) ,
0 commit comments