@@ -14,7 +14,7 @@ mod _csv {
1414 } ;
1515 use alloc:: fmt;
1616 use csv_core:: Terminator ;
17- use itertools:: { self , Itertools } ;
17+ use itertools:: Itertools ;
1818 use parking_lot:: Mutex ;
1919 use rustpython_common:: { lock:: LazyLock , wtf8:: Wtf8Buf } ;
2020 use rustpython_vm:: { match_class, sliceable:: SliceableSequenceOp } ;
@@ -409,7 +409,6 @@ mod _csv {
409409 output_ends : vec ! [ 0 ; 16 ] ,
410410 reader : options. to_reader ( ) ,
411411 skipinitialspace : options. get_skipinitialspace ( ) ,
412- delimiter : options. get_delimiter ( ) ,
413412 line_num : 0 ,
414413 } ) ,
415414 dialect : options. result ( vm) ?,
@@ -773,29 +772,6 @@ mod _csv {
773772 skipinitialspace
774773 }
775774
776- fn get_delimiter ( & self ) -> u8 {
777- let mut delimiter = match & self . dialect {
778- DialectItem :: Str ( name) => {
779- let g = GLOBAL_HASHMAP . lock ( ) ;
780- if let Some ( dialect) = g. get ( name) {
781- dialect. delimiter
782- // RustPython todo
783- // todo! Perfecting the remaining attributes.
784- } else {
785- b','
786- }
787- }
788- DialectItem :: Obj ( obj) => obj. delimiter ,
789- _ => b',' ,
790- } ;
791-
792- if let Some ( attr) = self . delimiter {
793- delimiter = attr
794- }
795-
796- delimiter
797- }
798-
799775 fn get_lineterminator ( & self ) -> csv_core:: Terminator {
800776 let mut lineterminator = match & self . dialect {
801777 DialectItem :: Str ( name) => {
@@ -959,7 +935,6 @@ mod _csv {
959935 output_ends : Vec < usize > ,
960936 reader : csv_core:: Reader ,
961937 skipinitialspace : bool ,
962- delimiter : u8 ,
963938 line_num : u64 ,
964939 }
965940
@@ -994,92 +969,131 @@ mod _csv {
994969
995970 impl SelfIter for Reader { }
996971
997- fn read_quote_record (
998- input : & [ u8 ] ,
999- dialect : PyDialect ,
1000- field_limit : isize ,
1001- vm : & VirtualMachine ,
1002- ) -> PyResult < Vec < PyObjectRef > > {
1003- // QUOTE_NOTNULL and QUOTE_STRINGS map empty unquoted fields to None,
1004- // but preserve quoted empty fields as strings, so retain quote provenance.
1005- let mut fields = vec ! [ ( Vec :: new( ) , false ) ] ;
1006- let mut at_field_start = true ;
1007- let mut in_quoted_field = false ;
1008- let mut escaped = false ;
1009- let mut index = 0 ;
972+ enum QuoteScanEvent {
973+ InitialSpace ,
974+ StartQuotedField ,
975+ EndQuotedField ,
976+ Escaped ( Option < u8 > ) ,
977+ DoubleQuote ( u8 ) ,
978+ Delimiter ,
979+ RecordTerminator ,
980+ Data ( u8 ) ,
981+ }
1010982
1011- while index < input. len ( ) {
1012- let byte = input[ index] ;
983+ struct QuoteScanState {
984+ at_field_start : bool ,
985+ in_quoted_field : bool ,
986+ }
1013987
1014- if escaped {
1015- fields. last_mut ( ) . unwrap ( ) . 0 . push ( byte) ;
1016- escaped = false ;
1017- at_field_start = false ;
1018- index += 1 ;
1019- continue ;
988+ impl QuoteScanState {
989+ const fn new ( ) -> Self {
990+ Self {
991+ at_field_start : true ,
992+ in_quoted_field : false ,
1020993 }
994+ }
995+
996+ fn scan (
997+ & mut self ,
998+ input : & [ u8 ] ,
999+ index : usize ,
1000+ dialect : PyDialect ,
1001+ unquoted_escape : bool ,
1002+ ) -> ( QuoteScanEvent , usize ) {
1003+ let byte = input[ index] ;
10211004
1022- if dialect. escapechar == Some ( byte) {
1023- escaped = true ;
1024- at_field_start = false ;
1025- index += 1 ;
1026- continue ;
1005+ if ( self . in_quoted_field || unquoted_escape) && dialect. escapechar == Some ( byte) {
1006+ self . at_field_start = false ;
1007+ return match input. get ( index + 1 ) . copied ( ) {
1008+ Some ( escaped) => ( QuoteScanEvent :: Escaped ( Some ( escaped) ) , 2 ) ,
1009+ None => ( QuoteScanEvent :: Escaped ( None ) , 1 ) ,
1010+ } ;
10271011 }
10281012
1029- if in_quoted_field {
1013+ if self . in_quoted_field {
10301014 if dialect. quotechar == Some ( byte) {
10311015 if dialect. doublequote && input. get ( index + 1 ) == Some ( & byte) {
1032- fields. last_mut ( ) . unwrap ( ) . 0 . push ( byte) ;
1033- index += 2 ;
1034- } else {
1035- in_quoted_field = false ;
1036- index += 1 ;
1016+ return ( QuoteScanEvent :: DoubleQuote ( byte) , 2 ) ;
10371017 }
1038- } else {
1039- fields. last_mut ( ) . unwrap ( ) . 0 . push ( byte) ;
1040- index += 1 ;
1018+ self . in_quoted_field = false ;
1019+ return ( QuoteScanEvent :: EndQuotedField , 1 ) ;
10411020 }
1042- continue ;
1021+ return ( QuoteScanEvent :: Data ( byte) , 1 ) ;
1022+ }
1023+
1024+ if self . at_field_start && dialect. skipinitialspace && byte == b' ' {
1025+ return ( QuoteScanEvent :: InitialSpace , 1 ) ;
10431026 }
10441027
1045- if at_field_start && dialect. skipinitialspace && byte == b' ' {
1046- index += 1 ;
1047- } else if at_field_start
1028+ if self . at_field_start
10481029 && dialect. quoting != QuoteStyle :: None
10491030 && dialect. quotechar == Some ( byte)
10501031 {
1051- fields. last_mut ( ) . unwrap ( ) . 1 = true ;
1052- at_field_start = false ;
1053- in_quoted_field = true ;
1054- index += 1 ;
1055- } else if byte == dialect. delimiter {
1056- fields. push ( ( Vec :: new ( ) , false ) ) ;
1057- at_field_start = true ;
1058- index += 1 ;
1059- } else if matches ! ( byte, b'\r' | b'\n' ) {
1060- if !input[ index..]
1061- . iter ( )
1062- . all ( |& byte| matches ! ( byte, b'\r' | b'\n' ) )
1063- {
1064- return Err ( new_csv_error (
1065- vm,
1066- concat ! (
1067- "new-line character seen in unquoted field" ,
1068- " - do you need to open the file in universal-newline mode?"
1069- ) ,
1070- ) ) ;
1071- }
1072- break ;
1032+ self . at_field_start = false ;
1033+ self . in_quoted_field = true ;
1034+ return ( QuoteScanEvent :: StartQuotedField , 1 ) ;
1035+ }
1036+
1037+ if byte == dialect. delimiter {
1038+ self . at_field_start = true ;
1039+ return ( QuoteScanEvent :: Delimiter , 1 ) ;
1040+ }
1041+
1042+ self . at_field_start = false ;
1043+ if matches ! ( byte, b'\r' | b'\n' ) {
1044+ ( QuoteScanEvent :: RecordTerminator , 1 )
10731045 } else {
1074- fields. last_mut ( ) . unwrap ( ) . 0 . push ( byte) ;
1075- at_field_start = false ;
1076- index += 1 ;
1046+ ( QuoteScanEvent :: Data ( byte) , 1 )
1047+ }
1048+ }
1049+ }
1050+
1051+ fn read_quote_record (
1052+ input : & [ u8 ] ,
1053+ dialect : PyDialect ,
1054+ field_limit : isize ,
1055+ vm : & VirtualMachine ,
1056+ ) -> PyResult < Vec < PyObjectRef > > {
1057+ // QUOTE_NOTNULL and QUOTE_STRINGS map empty unquoted fields to None,
1058+ // but preserve quoted empty fields as strings, so retain quote provenance.
1059+ let mut fields = vec ! [ ( Vec :: new( ) , false ) ] ;
1060+ let mut scan_state = QuoteScanState :: new ( ) ;
1061+ let mut dangling_escape = false ;
1062+ let mut index = 0 ;
1063+
1064+ while index < input. len ( ) {
1065+ let ( event, consumed) = scan_state. scan ( input, index, dialect, true ) ;
1066+ match event {
1067+ QuoteScanEvent :: InitialSpace | QuoteScanEvent :: EndQuotedField => { }
1068+ QuoteScanEvent :: StartQuotedField => fields. last_mut ( ) . unwrap ( ) . 1 = true ,
1069+ QuoteScanEvent :: Escaped ( Some ( byte) ) | QuoteScanEvent :: DoubleQuote ( byte) => {
1070+ fields. last_mut ( ) . unwrap ( ) . 0 . push ( byte) ;
1071+ }
1072+ QuoteScanEvent :: Escaped ( None ) => dangling_escape = true ,
1073+ QuoteScanEvent :: Delimiter => fields. push ( ( Vec :: new ( ) , false ) ) ,
1074+ QuoteScanEvent :: RecordTerminator => {
1075+ if !input[ index..]
1076+ . iter ( )
1077+ . all ( |& byte| matches ! ( byte, b'\r' | b'\n' ) )
1078+ {
1079+ return Err ( new_csv_error (
1080+ vm,
1081+ concat ! (
1082+ "new-line character seen in unquoted field" ,
1083+ " - do you need to open the file in universal-newline mode?"
1084+ ) ,
1085+ ) ) ;
1086+ }
1087+ break ;
1088+ }
1089+ QuoteScanEvent :: Data ( byte) => fields. last_mut ( ) . unwrap ( ) . 0 . push ( byte) ,
10771090 }
1091+ index += consumed;
10781092 }
10791093
10801094 // CPython treats an escape character at the end of an iterator item
10811095 // as escaping the implicit newline at the end of that item.
1082- if escaped {
1096+ if dangling_escape {
10831097 fields. last_mut ( ) . unwrap ( ) . 0 . push ( b'\n' ) ;
10841098 }
10851099
@@ -1124,7 +1138,6 @@ mod _csv {
11241138 output_ends,
11251139 reader,
11261140 skipinitialspace,
1127- delimiter,
11281141 line_num,
11291142 } = & mut * state;
11301143
@@ -1145,9 +1158,22 @@ mod _csv {
11451158 }
11461159
11471160 #[ inline]
1148- fn trim_initial_spaces ( input : & [ u8 ] ) -> & [ u8 ] {
1149- let trimmed_start = input. iter ( ) . position ( |& x| x != b' ' ) . unwrap_or ( input. len ( ) ) ;
1150- & input[ trimmed_start..]
1161+ fn trim_initial_spaces ( input : & [ u8 ] , dialect : PyDialect ) -> Vec < u8 > {
1162+ let mut trimmed = Vec :: with_capacity ( input. len ( ) ) ;
1163+ let mut scan_state = QuoteScanState :: new ( ) ;
1164+ let mut index = 0 ;
1165+
1166+ // Delimiters inside quoted fields are data, so only skip spaces
1167+ // after delimiters encountered outside quotes.
1168+ while index < input. len ( ) {
1169+ let ( event, consumed) = scan_state. scan ( input, index, dialect, false ) ;
1170+ if !matches ! ( event, QuoteScanEvent :: InitialSpace ) {
1171+ trimmed. extend_from_slice ( & input[ index..index + consumed] ) ;
1172+ }
1173+ index += consumed;
1174+ }
1175+
1176+ trimmed
11511177 }
11521178
11531179 #[ inline]
@@ -1162,12 +1188,7 @@ mod _csv {
11621188 }
11631189
11641190 let input = if * skipinitialspace {
1165- let t = input. split ( |x| x == delimiter) ;
1166- t. map ( |x| {
1167- let trimmed = trim_initial_spaces ( x) ;
1168- String :: from_utf8 ( trimmed. to_vec ( ) ) . unwrap ( )
1169- } )
1170- . join ( format ! ( "{}" , * delimiter as char ) . as_str ( ) )
1191+ String :: from_utf8 ( trim_initial_spaces ( input, zelf. dialect ) ) . unwrap ( )
11711192 } else {
11721193 String :: from_utf8 ( input. to_vec ( ) ) . unwrap ( )
11731194 } ;
0 commit comments