@@ -570,6 +570,151 @@ pub(crate) mod module {
570570 Ok ( path. mode . process_path ( buffer. to_os_string ( ) , vm) )
571571 }
572572
573+ /// Implements CPython's _Py_skiproot logic for Windows paths
574+ /// Returns (drive_size, root_size) where:
575+ /// - drive_size: length of the drive/UNC portion
576+ /// - root_size: length of the root separator (0 or 1)
577+ fn skiproot ( path : & [ u16 ] ) -> ( usize , usize ) {
578+ let len = path. len ( ) ;
579+ if len == 0 {
580+ return ( 0 , 0 ) ;
581+ }
582+
583+ const SEP : u16 = b'\\' as u16 ;
584+ const ALTSEP : u16 = b'/' as u16 ;
585+ const COLON : u16 = b':' as u16 ;
586+
587+ let is_sep = |c : u16 | c == SEP || c == ALTSEP ;
588+ let get = |i : usize | path. get ( i) . copied ( ) . unwrap_or ( 0 ) ;
589+
590+ if is_sep ( get ( 0 ) ) {
591+ if is_sep ( get ( 1 ) ) {
592+ // UNC or device path: \\server\share or \\?\device
593+ // Check for \\?\UNC\server\share
594+ let idx = if len >= 8
595+ && get ( 2 ) == b'?' as u16
596+ && is_sep ( get ( 3 ) )
597+ && ( get ( 4 ) == b'U' as u16 || get ( 4 ) == b'u' as u16 )
598+ && ( get ( 5 ) == b'N' as u16 || get ( 5 ) == b'n' as u16 )
599+ && ( get ( 6 ) == b'C' as u16 || get ( 6 ) == b'c' as u16 )
600+ && is_sep ( get ( 7 ) )
601+ {
602+ 8
603+ } else {
604+ 2
605+ } ;
606+
607+ // Find the end of server name
608+ let mut i = idx;
609+ while i < len && !is_sep ( get ( i) ) {
610+ i += 1 ;
611+ }
612+
613+ if i >= len {
614+ // No share part: \\server
615+ return ( i, 0 ) ;
616+ }
617+
618+ // Skip separator and find end of share name
619+ i += 1 ;
620+ while i < len && !is_sep ( get ( i) ) {
621+ i += 1 ;
622+ }
623+
624+ // drive = \\server\share, root = \ (if present)
625+ if i >= len { ( i, 0 ) } else { ( i, 1 ) }
626+ } else {
627+ // Relative path with root: \Windows
628+ ( 0 , 1 )
629+ }
630+ } else if len >= 2 && get ( 1 ) == COLON {
631+ // Drive letter path
632+ if len >= 3 && is_sep ( get ( 2 ) ) {
633+ // Absolute: X:\Windows
634+ ( 2 , 1 )
635+ } else {
636+ // Relative with drive: X:Windows
637+ ( 2 , 0 )
638+ }
639+ } else {
640+ // Relative path: Windows
641+ ( 0 , 0 )
642+ }
643+ }
644+
645+ #[ pyfunction]
646+ fn _path_splitroot_ex ( path : crate :: PyObjectRef , vm : & VirtualMachine ) -> PyResult < PyTupleRef > {
647+ use crate :: builtins:: { PyBytes , PyStr } ;
648+ use rustpython_common:: wtf8:: Wtf8Buf ;
649+
650+ // Handle path-like objects via os.fspath, but without null check (nonstrict=True in CPython)
651+ let path = if let Some ( fspath) = vm. get_method ( path. clone ( ) , identifier ! ( vm, __fspath__) ) {
652+ fspath?. call ( ( ) , vm) ?
653+ } else {
654+ path
655+ } ;
656+
657+ // Convert to wide string, validating UTF-8 for bytes input
658+ let ( wide, is_bytes) : ( Vec < u16 > , bool ) = if let Some ( s) = path. downcast_ref :: < PyStr > ( ) {
659+ // Use encode_wide which handles WTF-8 (including surrogates)
660+ let wide: Vec < u16 > = s. as_wtf8 ( ) . encode_wide ( ) . collect ( ) ;
661+ ( wide, false )
662+ } else if let Some ( b) = path. downcast_ref :: < PyBytes > ( ) {
663+ // On Windows, bytes must be valid UTF-8 - this raises UnicodeDecodeError if not
664+ let s = std:: str:: from_utf8 ( b. as_bytes ( ) ) . map_err ( |e| {
665+ vm. new_exception_msg (
666+ vm. ctx . exceptions . unicode_decode_error . to_owned ( ) ,
667+ format ! (
668+ "'utf-8' codec can't decode byte {:#x} in position {}: invalid start byte" ,
669+ b. as_bytes( ) . get( e. valid_up_to( ) ) . copied( ) . unwrap_or( 0 ) ,
670+ e. valid_up_to( )
671+ ) ,
672+ )
673+ } ) ?;
674+ let wide: Vec < u16 > = s. encode_utf16 ( ) . collect ( ) ;
675+ ( wide, true )
676+ } else {
677+ return Err ( vm. new_type_error ( format ! (
678+ "expected str or bytes, not {}" ,
679+ path. class( ) . name( )
680+ ) ) ) ;
681+ } ;
682+
683+ // Normalize slashes for parsing
684+ let normalized: Vec < u16 > = wide
685+ . iter ( )
686+ . map ( |& c| if c == b'/' as u16 { b'\\' as u16 } else { c } )
687+ . collect ( ) ;
688+
689+ let ( drv_size, root_size) = skiproot ( & normalized) ;
690+
691+ // Return as bytes if input was bytes, preserving the original content
692+ if is_bytes {
693+ // Convert UTF-16 back to UTF-8 for bytes output
694+ let drv = String :: from_utf16 ( & wide[ ..drv_size] )
695+ . map_err ( |e| vm. new_unicode_decode_error ( e. to_string ( ) ) ) ?;
696+ let root = String :: from_utf16 ( & wide[ drv_size..drv_size + root_size] )
697+ . map_err ( |e| vm. new_unicode_decode_error ( e. to_string ( ) ) ) ?;
698+ let tail = String :: from_utf16 ( & wide[ drv_size + root_size..] )
699+ . map_err ( |e| vm. new_unicode_decode_error ( e. to_string ( ) ) ) ?;
700+ Ok ( vm. ctx . new_tuple ( vec ! [
701+ vm. ctx. new_bytes( drv. into_bytes( ) ) . into( ) ,
702+ vm. ctx. new_bytes( root. into_bytes( ) ) . into( ) ,
703+ vm. ctx. new_bytes( tail. into_bytes( ) ) . into( ) ,
704+ ] ) )
705+ } else {
706+ // For str output, use WTF-8 to handle surrogates
707+ let drv = Wtf8Buf :: from_wide ( & wide[ ..drv_size] ) ;
708+ let root = Wtf8Buf :: from_wide ( & wide[ drv_size..drv_size + root_size] ) ;
709+ let tail = Wtf8Buf :: from_wide ( & wide[ drv_size + root_size..] ) ;
710+ Ok ( vm. ctx . new_tuple ( vec ! [
711+ vm. ctx. new_str( drv) . into( ) ,
712+ vm. ctx. new_str( root) . into( ) ,
713+ vm. ctx. new_str( tail) . into( ) ,
714+ ] ) )
715+ }
716+ }
717+
573718 #[ pyfunction]
574719 fn _path_splitroot ( path : OsPath , vm : & VirtualMachine ) -> PyResult < ( String , String ) > {
575720 let orig: Vec < _ > = path. path . to_wide ( ) ;
0 commit comments