@@ -382,6 +382,7 @@ System site-packages will not be used for module resolution.",
382
382
/// See also: <https://snarky.ca/how-virtual-environments-work/>
383
383
#[ derive( Debug ) ]
384
384
struct PyvenvCfgParser < ' s > {
385
+ source : & ' s str ,
385
386
cursor : Cursor < ' s > ,
386
387
line_number : NonZeroUsize ,
387
388
data : RawPyvenvCfg < ' s > ,
@@ -390,6 +391,7 @@ struct PyvenvCfgParser<'s> {
390
391
impl < ' s > PyvenvCfgParser < ' s > {
391
392
fn new ( source : & ' s str ) -> Self {
392
393
Self {
394
+ source,
393
395
cursor : Cursor :: new ( source) ,
394
396
line_number : NonZeroUsize :: new ( 1 ) . unwrap ( ) ,
395
397
data : RawPyvenvCfg :: default ( ) ,
@@ -409,6 +411,7 @@ impl<'s> PyvenvCfgParser<'s> {
409
411
/// to the beginning of the next line.
410
412
fn parse_line ( & mut self ) -> Result < ( ) , PyvenvCfgParseErrorKind > {
411
413
let PyvenvCfgParser {
414
+ source,
412
415
cursor,
413
416
line_number,
414
417
data,
@@ -418,35 +421,24 @@ impl<'s> PyvenvCfgParser<'s> {
418
421
419
422
cursor. eat_while ( |c| c. is_whitespace ( ) && c != '\n' ) ;
420
423
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 ( ) ;
422
427
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 ( '=' ) {
430
429
// Skip over any lines that do not contain '=' characters, same as the CPython stdlib
431
430
// <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
-
440
431
cursor. eat_char ( '\n' ) ;
441
432
return Ok ( ( ) ) ;
442
- } ;
433
+ }
443
434
444
- let key = remaining_file [ ..eq_position ] . trim ( ) ;
435
+ let key = source [ TextRange :: new ( key_start , key_end ) ] . trim ( ) ;
445
436
446
- cursor. skip_bytes ( eq_position + 1 ) ;
447
437
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' ) ;
450
442
451
443
if value. is_empty ( ) {
452
444
return Err ( PyvenvCfgParseErrorKind :: MalformedKeyValuePair { line_number } ) ;
@@ -460,7 +452,7 @@ impl<'s> PyvenvCfgParser<'s> {
460
452
// `virtualenv` and `uv` call this key `version_info`,
461
453
// but the stdlib venv module calls it `version`
462
454
"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 ( ) ) ;
464
456
data. version = Some ( ( value, version_range) ) ;
465
457
}
466
458
"implementation" => {
@@ -479,8 +471,6 @@ impl<'s> PyvenvCfgParser<'s> {
479
471
_ => { }
480
472
}
481
473
482
- cursor. eat_while ( |c| c != '\n' ) ;
483
- cursor. eat_char ( '\n' ) ;
484
474
Ok ( ( ) )
485
475
}
486
476
}
@@ -1581,4 +1571,18 @@ mod tests {
1581
1571
assert_eq ! ( & pyvenv_cfg[ version. 1 ] , version. 0 ) ;
1582
1572
assert_eq ! ( parsed. implementation, PythonImplementation :: PyPy ) ;
1583
1573
}
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
+ }
1584
1588
}
0 commit comments