@@ -382,6 +382,7 @@ System site-packages will not be used for module resolution.",
382382/// See also: <https://snarky.ca/how-virtual-environments-work/>
383383#[ derive( Debug ) ]
384384struct PyvenvCfgParser < ' s > {
385+ source : & ' s str ,
385386 cursor : Cursor < ' s > ,
386387 line_number : NonZeroUsize ,
387388 data : RawPyvenvCfg < ' s > ,
@@ -390,6 +391,7 @@ struct PyvenvCfgParser<'s> {
390391impl < ' s > PyvenvCfgParser < ' s > {
391392 fn new ( source : & ' s str ) -> Self {
392393 Self {
394+ source,
393395 cursor : Cursor :: new ( source) ,
394396 line_number : NonZeroUsize :: new ( 1 ) . unwrap ( ) ,
395397 data : RawPyvenvCfg :: default ( ) ,
@@ -409,6 +411,7 @@ impl<'s> PyvenvCfgParser<'s> {
409411 /// to the beginning of the next line.
410412 fn parse_line ( & mut self ) -> Result < ( ) , PyvenvCfgParseErrorKind > {
411413 let PyvenvCfgParser {
414+ source,
412415 cursor,
413416 line_number,
414417 data,
@@ -418,35 +421,24 @@ impl<'s> PyvenvCfgParser<'s> {
418421
419422 cursor. eat_while ( |c| c. is_whitespace ( ) && c != '\n' ) ;
420423
421- let remaining_file = cursor. chars ( ) . as_str ( ) ;
424+ let key_start = cursor. offset ( ) ;
425+ cursor. eat_while ( |c| !matches ! ( c, '\n' | '=' ) ) ;
426+ let key_end = cursor. offset ( ) ;
422427
423- let next_newline_position = remaining_file
424- . find ( '\n' )
425- . unwrap_or ( cursor. text_len ( ) . to_usize ( ) ) ;
426-
427- // The Python standard-library's `site` module parses these files by splitting each line on
428- // '=' characters, so that's what we should do as well.
429- let Some ( eq_position) = remaining_file[ ..next_newline_position] . find ( '=' ) else {
428+ if !cursor. eat_char ( '=' ) {
430429 // Skip over any lines that do not contain '=' characters, same as the CPython stdlib
431430 // <https://github.com/python/cpython/blob/e64395e8eb8d3a9e35e3e534e87d427ff27ab0a5/Lib/site.py#L625-L632>
432- cursor. skip_bytes ( next_newline_position) ;
433-
434- debug_assert ! (
435- matches!( cursor. first( ) , '\n' | ruff_python_trivia:: EOF_CHAR , ) ,
436- "{}" ,
437- cursor. first( )
438- ) ;
439-
440431 cursor. eat_char ( '\n' ) ;
441432 return Ok ( ( ) ) ;
442- } ;
433+ }
443434
444- let key = remaining_file [ ..eq_position ] . trim ( ) ;
435+ let key = source [ TextRange :: new ( key_start , key_end ) ] . trim ( ) ;
445436
446- cursor. skip_bytes ( eq_position + 1 ) ;
447437 cursor. eat_while ( |c| c. is_whitespace ( ) && c != '\n' ) ;
448-
449- let value = remaining_file[ eq_position + 1 ..next_newline_position] . trim ( ) ;
438+ let value_start = cursor. offset ( ) ;
439+ cursor. eat_while ( |c| c != '\n' ) ;
440+ let value = source[ TextRange :: new ( value_start, cursor. offset ( ) ) ] . trim ( ) ;
441+ cursor. eat_char ( '\n' ) ;
450442
451443 if value. is_empty ( ) {
452444 return Err ( PyvenvCfgParseErrorKind :: MalformedKeyValuePair { line_number } ) ;
@@ -460,7 +452,7 @@ impl<'s> PyvenvCfgParser<'s> {
460452 // `virtualenv` and `uv` call this key `version_info`,
461453 // but the stdlib venv module calls it `version`
462454 "version" | "version_info" => {
463- let version_range = TextRange :: at ( cursor . offset ( ) , value. text_len ( ) ) ;
455+ let version_range = TextRange :: at ( value_start , value. text_len ( ) ) ;
464456 data. version = Some ( ( value, version_range) ) ;
465457 }
466458 "implementation" => {
@@ -479,8 +471,6 @@ impl<'s> PyvenvCfgParser<'s> {
479471 _ => { }
480472 }
481473
482- cursor. eat_while ( |c| c != '\n' ) ;
483- cursor. eat_char ( '\n' ) ;
484474 Ok ( ( ) )
485475 }
486476}
@@ -1581,4 +1571,18 @@ mod tests {
15811571 assert_eq ! ( & pyvenv_cfg[ version. 1 ] , version. 0 ) ;
15821572 assert_eq ! ( parsed. implementation, PythonImplementation :: PyPy ) ;
15831573 }
1574+
1575+ #[ test]
1576+ fn pyvenv_cfg_with_strange_whitespace_parses ( ) {
1577+ let pyvenv_cfg = " home= /a path with whitespace/python\t \t \n version_info = 3.13 \n \n \n \n implementation =PyPy" ;
1578+ let parsed = PyvenvCfgParser :: new ( pyvenv_cfg) . parse ( ) . unwrap ( ) ;
1579+ assert_eq ! (
1580+ parsed. base_executable_home_path,
1581+ Some ( "/a path with whitespace/python" )
1582+ ) ;
1583+ let version = parsed. version . unwrap ( ) ;
1584+ assert_eq ! ( version. 0 , "3.13" ) ;
1585+ assert_eq ! ( & pyvenv_cfg[ version. 1 ] , version. 0 ) ;
1586+ assert_eq ! ( parsed. implementation, PythonImplementation :: PyPy ) ;
1587+ }
15841588}
0 commit comments