Skip to main content

ruff_db/
vendored.rs

1use std::borrow::Cow;
2use std::collections::BTreeMap;
3use std::fmt::{self, Debug};
4use std::io::{self, Read, Write};
5use std::sync::Arc;
6
7use zip::result::{ZipError, ZipResult};
8use zip::write::SimpleFileOptions;
9use zip::{CompressionMethod, ZipArchive, ZipWriter, read::ZipFile};
10
11pub use self::path::{VendoredPath, VendoredPathBuf};
12use crate::file_revision::FileRevision;
13
14mod path;
15
16type Result<T> = io::Result<T>;
17
18/// File system that stores all content in a static zip archive
19/// bundled as part of the Ruff binary.
20///
21/// "Files" in the `VendoredFileSystem` are read-only and immutable.
22/// Directories are supported, but symlinks and hardlinks cannot exist.
23///
24/// # Path separators
25///
26/// At time of writing (2025-07-11), this implementation always uses `/` as a
27/// path separator, even in Windows environments where `\` is traditionally
28/// used as a file path separator. Namely, this is only currently used with zip
29/// files built by `crates/ty_vendored/build.rs`.
30///
31/// Callers using this may provide paths that use a `\` as a separator. It will
32/// be transparently normalized to `/`.
33///
34/// This is particularly important because the presence of a trailing separator
35/// in a zip file is conventionally used to indicate a directory entry.
36#[derive(Clone)]
37pub struct VendoredFileSystem {
38    inner: Arc<VendoredZipArchive>,
39}
40
41impl VendoredFileSystem {
42    pub fn new_static(raw_bytes: &'static [u8]) -> Result<Self> {
43        Self::new_impl(ArchiveData::Static(raw_bytes))
44    }
45
46    pub fn new(raw_bytes: Vec<u8>) -> Result<Self> {
47        Self::new_impl(ArchiveData::Owned(raw_bytes.into()))
48    }
49
50    fn new_impl(data: ArchiveData) -> Result<Self> {
51        Ok(Self {
52            inner: Arc::new(VendoredZipArchive::new(data)?),
53        })
54    }
55
56    pub fn exists(&self, path: impl AsRef<VendoredPath>) -> bool {
57        fn exists(fs: &VendoredFileSystem, path: &VendoredPath) -> bool {
58            let normalized = NormalizedVendoredPath::from(path);
59            let archive = &fs.inner;
60
61            // Must probe the zipfile twice, as "stdlib" and "stdlib/" are considered
62            // different paths in a zip file, but we want to abstract over that difference here
63            // so that paths relative to the `VendoredFileSystem`
64            // work the same as other paths in Ruff.
65            archive.index_for_path(&normalized).is_some()
66                || archive
67                    .index_for_path(&normalized.with_trailing_slash())
68                    .is_some()
69        }
70
71        exists(self, path.as_ref())
72    }
73
74    pub fn metadata(&self, path: impl AsRef<VendoredPath>) -> Result<Metadata> {
75        fn metadata(fs: &VendoredFileSystem, path: &VendoredPath) -> Result<Metadata> {
76            let normalized = NormalizedVendoredPath::from(path);
77            let mut archive = fs.archive_reader();
78
79            // Must probe the zipfile twice, as "stdlib" and "stdlib/" are considered
80            // different paths in a zip file, but we want to abstract over that difference here
81            // so that paths relative to the `VendoredFileSystem`
82            // work the same as other paths in Ruff.
83            if let Ok(metadata) = archive.metadata_for_path(&normalized) {
84                return Ok(metadata);
85            }
86            archive.metadata_for_path(&normalized.with_trailing_slash())
87        }
88
89        metadata(self, path.as_ref())
90    }
91
92    pub fn is_directory(&self, path: impl AsRef<VendoredPath>) -> bool {
93        self.metadata(path)
94            .is_ok_and(|metadata| metadata.kind().is_directory())
95    }
96
97    pub fn is_file(&self, path: impl AsRef<VendoredPath>) -> bool {
98        self.metadata(path)
99            .is_ok_and(|metadata| metadata.kind().is_file())
100    }
101
102    /// Read the entire contents of the zip file at `path` into a string
103    ///
104    /// Returns an Err() if any of the following are true:
105    /// - The path does not exist in the underlying zip archive
106    /// - The path exists in the underlying zip archive, but represents a directory
107    /// - The contents of the zip file at `path` contain invalid UTF-8
108    pub fn read_to_string(&self, path: impl AsRef<VendoredPath>) -> Result<String> {
109        fn read_to_string(fs: &VendoredFileSystem, path: &VendoredPath) -> Result<String> {
110            let mut archive = fs.archive_reader();
111            let mut zip_file = archive.lookup_path(&NormalizedVendoredPath::from(path))?;
112
113            // Pre-allocate the buffer with the size specified in the ZIP file metadata
114            // because `read_to_string` passes `None` as the size hint.
115            // But let's not trust the zip file metadata (even though it's vendored)
116            // and limit it to a reasonable size.
117            let mut buffer = String::with_capacity(
118                usize::try_from(zip_file.size())
119                    .unwrap_or(usize::MAX)
120                    .min(10_000_000),
121            );
122            zip_file.read_to_string(&mut buffer)?;
123            Ok(buffer)
124        }
125
126        read_to_string(self, path.as_ref())
127    }
128
129    /// Read the direct children of the directory
130    /// identified by `path`.
131    ///
132    /// If `path` is not a directory, then this will
133    /// return an empty iterator.
134    pub fn read_directory(
135        &self,
136        dir: impl AsRef<VendoredPath>,
137    ) -> impl Iterator<Item = DirectoryEntry> + '_ {
138        let directory_prefix = NormalizedVendoredPath::from(dir.as_ref())
139            .with_trailing_slash()
140            .0
141            .into_owned();
142
143        self.inner.0.file_names().filter_map(move |name| {
144            // Any entry that doesn't have the `path` (with a
145            // trailing slash) as a prefix cannot possibly be in
146            // the directory referenced by `path`.
147            let without_dir_prefix = name.strip_prefix(&directory_prefix)?;
148            // Filter out an entry equivalent to the path given
149            // since we only want children of the directory.
150            if without_dir_prefix.is_empty() {
151                return None;
152            }
153            // We only want *direct* children. Files that are
154            // direct children cannot have any slashes (or else
155            // they are not direct children). Directories that
156            // are direct children can only have one slash and
157            // it must be at the end.
158            //
159            // (We do this manually ourselves to avoid doing a
160            // full file lookup and metadata retrieval via the
161            // `zip` crate.)
162            let file_type = FileType::from_zip_file_name(without_dir_prefix);
163            let slash_count = without_dir_prefix.matches('/').count();
164            match file_type {
165                FileType::File if slash_count > 0 => return None,
166                FileType::Directory if slash_count > 1 => return None,
167                _ => {}
168            }
169
170            Some(DirectoryEntry {
171                path: VendoredPathBuf::from(name),
172                file_type,
173            })
174        })
175    }
176
177    /// Creates a reader with its own cursor over the shared archive data.
178    ///
179    /// `ZipArchive` stores the current seek position in its reader. Cloning it gives each operation
180    /// an independent cursor, so reads can seek and decompress files concurrently without
181    /// synchronizing access to shared reader state.
182    ///
183    /// The clone is cheap: `ZipArchive` shares its parsed central-directory metadata, while
184    /// `ArchiveData` either copies a static reference or increments an `Arc` reference count. The
185    /// ZIP bytes themselves are never copied.
186    fn archive_reader(&self) -> VendoredZipArchive {
187        self.inner.as_ref().clone()
188    }
189}
190
191impl fmt::Debug for VendoredFileSystem {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        if f.alternate() {
194            let mut archive = self.archive_reader();
195            let mut paths: Vec<String> = archive.0.file_names().map(String::from).collect();
196            paths.sort();
197            let debug_info: BTreeMap<String, ZipFileDebugInfo> = paths
198                .iter()
199                .map(|path| {
200                    (
201                        path.to_owned(),
202                        ZipFileDebugInfo::from(archive.0.by_name(path).unwrap()),
203                    )
204                })
205                .collect();
206            f.debug_struct("VendoredFileSystem")
207                .field("paths", &paths)
208                .field("data_by_path", &debug_info)
209                .finish()
210        } else {
211            write!(f, "VendoredFileSystem(<{} paths>)", self.inner.len())
212        }
213    }
214}
215
216impl Default for VendoredFileSystem {
217    fn default() -> Self {
218        let mut bytes: Vec<u8> = Vec::new();
219        let mut cursor = io::Cursor::new(&mut bytes);
220
221        {
222            let writer = ZipWriter::new(&mut cursor);
223            writer.finish().unwrap();
224        }
225
226        VendoredFileSystem::new(bytes).unwrap()
227    }
228}
229
230/// Private struct only used in `Debug` implementations
231///
232/// This could possibly be unified with the `Metadata` struct,
233/// but that is deliberately kept small, and only exposes metadata
234/// that users of the `VendoredFileSystem` could realistically need.
235/// For debugging purposes, however, we want to have all information
236/// available.
237#[expect(unused)]
238#[derive(Debug)]
239struct ZipFileDebugInfo {
240    crc32_hash: u32,
241    compressed_size: u64,
242    uncompressed_size: u64,
243    kind: FileType,
244}
245
246impl<'a, R: Read> From<ZipFile<'a, R>> for ZipFileDebugInfo {
247    fn from(value: ZipFile<'a, R>) -> Self {
248        Self {
249            crc32_hash: value.crc32(),
250            compressed_size: value.compressed_size(),
251            uncompressed_size: value.size(),
252            kind: if value.is_dir() {
253                FileType::Directory
254            } else {
255                FileType::File
256            },
257        }
258    }
259}
260
261#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
262pub enum FileType {
263    /// The path exists in the zip archive and represents a vendored file
264    File,
265
266    /// The path exists in the zip archive and represents a vendored directory of files
267    Directory,
268}
269
270impl FileType {
271    fn from_zip_file_name(name: &str) -> FileType {
272        if name.ends_with('/') {
273            FileType::Directory
274        } else {
275            FileType::File
276        }
277    }
278
279    pub const fn is_file(self) -> bool {
280        matches!(self, Self::File)
281    }
282
283    pub const fn is_directory(self) -> bool {
284        matches!(self, Self::Directory)
285    }
286}
287
288#[derive(Clone, Copy, Debug, PartialEq, Eq)]
289pub struct Metadata {
290    kind: FileType,
291    revision: FileRevision,
292}
293
294impl Metadata {
295    fn from_zip_file<R: Read>(zip_file: ZipFile<'_, R>) -> Self {
296        let kind = if zip_file.is_dir() {
297            FileType::Directory
298        } else {
299            FileType::File
300        };
301
302        Self {
303            kind,
304            revision: FileRevision::new(u128::from(zip_file.crc32())),
305        }
306    }
307
308    pub fn kind(&self) -> FileType {
309        self.kind
310    }
311
312    pub fn revision(&self) -> FileRevision {
313        self.revision
314    }
315}
316
317#[derive(Debug, PartialEq, Eq)]
318pub struct DirectoryEntry {
319    path: VendoredPathBuf,
320    file_type: FileType,
321}
322
323impl DirectoryEntry {
324    pub fn new(path: VendoredPathBuf, file_type: FileType) -> Self {
325        Self { path, file_type }
326    }
327
328    pub fn into_path(self) -> VendoredPathBuf {
329        self.path
330    }
331
332    pub fn path(&self) -> &VendoredPath {
333        &self.path
334    }
335
336    pub fn file_type(&self) -> FileType {
337        self.file_type
338    }
339}
340
341/// Immutable archive data that is cheap to clone into an independent reader.
342#[derive(Clone, Debug)]
343enum ArchiveData {
344    Static(&'static [u8]),
345    Owned(Arc<[u8]>),
346}
347
348impl AsRef<[u8]> for ArchiveData {
349    fn as_ref(&self) -> &[u8] {
350        match self {
351            Self::Static(data) => data,
352            Self::Owned(data) => data,
353        }
354    }
355}
356
357/// Newtype wrapper around a ZipArchive.
358#[derive(Clone, Debug)]
359struct VendoredZipArchive(ZipArchive<io::Cursor<ArchiveData>>);
360
361impl VendoredZipArchive {
362    fn new(data: ArchiveData) -> Result<Self> {
363        Ok(Self(ZipArchive::new(io::Cursor::new(data))?))
364    }
365
366    fn index_for_path(&self, path: &NormalizedVendoredPath) -> Option<usize> {
367        self.0.index_for_name(path.as_str())
368    }
369
370    fn lookup_path(
371        &mut self,
372        path: &NormalizedVendoredPath,
373    ) -> Result<ZipFile<'_, io::Cursor<ArchiveData>>> {
374        Ok(self.0.by_name(path.as_str())?)
375    }
376
377    fn metadata_for_path(&mut self, path: &NormalizedVendoredPath) -> Result<Metadata> {
378        let index = self.index_for_path(path).ok_or(ZipError::FileNotFound)?;
379        Ok(Metadata::from_zip_file(self.0.by_index_raw(index)?))
380    }
381
382    fn len(&self) -> usize {
383        self.0.len()
384    }
385}
386
387/// A path that has been normalized via the `normalize_vendored_path` function.
388///
389/// Trailing slashes are normalized away by `camino::Utf8PathBuf`s,
390/// but trailing slashes are crucial for distinguishing between
391/// files and directories inside zip archives.
392#[derive(Debug, Clone, PartialEq, Eq)]
393struct NormalizedVendoredPath<'a>(Cow<'a, str>);
394
395impl NormalizedVendoredPath<'_> {
396    fn with_trailing_slash(self) -> Self {
397        debug_assert!(!self.0.ends_with('/'));
398        let mut data = self.0.into_owned();
399        data.push('/');
400        Self(Cow::Owned(data))
401    }
402
403    fn as_str(&self) -> &str {
404        &self.0
405    }
406}
407
408impl<'a> From<&'a VendoredPath> for NormalizedVendoredPath<'a> {
409    /// Normalize the path.
410    ///
411    /// The normalizations are:
412    /// - Remove `.` and `..` components
413    /// - Strip trailing slashes
414    /// - Normalize `\\` separators to `/`
415    /// - Validate that the path does not have any unsupported components
416    ///
417    /// ## Panics:
418    /// If a path with an unsupported component for vendored paths is passed.
419    /// Unsupported components are path prefixes and path root directories.
420    fn from(path: &'a VendoredPath) -> Self {
421        /// Remove `.` and `..` components, and validate that unsupported components are not present.
422        ///
423        /// This inner routine also strips trailing slashes,
424        /// and normalizes paths to use Unix `/` separators.
425        /// However, it always allocates, so avoid calling it if possible.
426        /// In most cases, the path should already be normalized.
427        fn normalize_unnormalized_path(path: &VendoredPath) -> String {
428            let mut normalized_parts = Vec::new();
429            for component in path.components() {
430                match component {
431                    camino::Utf8Component::Normal(part) => normalized_parts.push(part),
432                    camino::Utf8Component::CurDir => continue,
433                    camino::Utf8Component::ParentDir => {
434                        // `VendoredPath("")`, `VendoredPath("..")` and `VendoredPath("../..")`
435                        // all resolve to the same path relative to the zip archive
436                        // (see https://github.com/astral-sh/ruff/pull/11991#issuecomment-2185278014)
437                        normalized_parts.pop();
438                    }
439                    unsupported => {
440                        panic!("Unsupported component in a vendored path: {unsupported}")
441                    }
442                }
443            }
444            normalized_parts.join("/")
445        }
446
447        let path_str = path.as_str();
448
449        if std::path::MAIN_SEPARATOR == '\\' && path_str.contains('\\') {
450            // Normalize paths so that they always use Unix path separators
451            NormalizedVendoredPath(Cow::Owned(normalize_unnormalized_path(path)))
452        } else if !path
453            .components()
454            .all(|component| matches!(component, camino::Utf8Component::Normal(_)))
455        {
456            // Remove non-`Normal` components
457            NormalizedVendoredPath(Cow::Owned(normalize_unnormalized_path(path)))
458        } else {
459            // Strip trailing slashes from the path
460            NormalizedVendoredPath(Cow::Borrowed(path_str.trim_end_matches('/')))
461        }
462    }
463}
464
465pub struct VendoredFileSystemBuilder {
466    writer: ZipWriter<io::Cursor<Vec<u8>>>,
467    compression_method: CompressionMethod,
468}
469
470impl VendoredFileSystemBuilder {
471    pub fn new(compression_method: CompressionMethod) -> Self {
472        let buffer = io::Cursor::new(Vec::new());
473
474        Self {
475            writer: ZipWriter::new(buffer),
476            compression_method,
477        }
478    }
479
480    pub fn add_file(
481        &mut self,
482        path: impl AsRef<VendoredPath>,
483        content: &str,
484    ) -> std::io::Result<()> {
485        self.writer
486            .start_file(path.as_ref().as_str(), self.options())?;
487        self.writer.write_all(content.as_bytes())
488    }
489
490    pub fn add_directory(&mut self, path: impl AsRef<VendoredPath>) -> ZipResult<()> {
491        self.writer
492            .add_directory(path.as_ref().as_str(), self.options())
493    }
494
495    pub fn finish(self) -> Result<VendoredFileSystem> {
496        let buffer = self.writer.finish()?;
497
498        VendoredFileSystem::new(buffer.into_inner())
499    }
500
501    fn options(&self) -> SimpleFileOptions {
502        SimpleFileOptions::default()
503            .compression_method(self.compression_method)
504            .unix_permissions(0o644)
505    }
506}
507
508#[cfg(test)]
509pub(crate) mod tests {
510
511    use insta::assert_snapshot;
512
513    use super::*;
514
515    const FUNCTOOLS_CONTENTS: &str = "def update_wrapper(): ...";
516    const ASYNCIO_TASKS_CONTENTS: &str = "class Task: ...";
517
518    fn mock_typeshed() -> VendoredFileSystem {
519        let mut builder = VendoredFileSystemBuilder::new(CompressionMethod::Stored);
520
521        builder.add_directory("stdlib/").unwrap();
522        builder
523            .add_file("stdlib/functools.pyi", FUNCTOOLS_CONTENTS)
524            .unwrap();
525        builder.add_directory("stdlib/asyncio/").unwrap();
526        builder
527            .add_file("stdlib/asyncio/tasks.pyi", ASYNCIO_TASKS_CONTENTS)
528            .unwrap();
529
530        builder.finish().unwrap()
531    }
532
533    #[test]
534    fn filesystem_debug_implementation() {
535        assert_snapshot!(
536            format!("{:?}", mock_typeshed()),
537            @"VendoredFileSystem(<4 paths>)"
538        );
539    }
540
541    #[test]
542    fn filesystem_debug_implementation_alternate() {
543        assert_snapshot!(format!("{:#?}", mock_typeshed()), @r#"
544        VendoredFileSystem {
545            paths: [
546                "stdlib/",
547                "stdlib/asyncio/",
548                "stdlib/asyncio/tasks.pyi",
549                "stdlib/functools.pyi",
550            ],
551            data_by_path: {
552                "stdlib/": ZipFileDebugInfo {
553                    crc32_hash: 0,
554                    compressed_size: 0,
555                    uncompressed_size: 0,
556                    kind: Directory,
557                },
558                "stdlib/asyncio/": ZipFileDebugInfo {
559                    crc32_hash: 0,
560                    compressed_size: 0,
561                    uncompressed_size: 0,
562                    kind: Directory,
563                },
564                "stdlib/asyncio/tasks.pyi": ZipFileDebugInfo {
565                    crc32_hash: 2826547428,
566                    compressed_size: 15,
567                    uncompressed_size: 15,
568                    kind: File,
569                },
570                "stdlib/functools.pyi": ZipFileDebugInfo {
571                    crc32_hash: 1099005079,
572                    compressed_size: 25,
573                    uncompressed_size: 25,
574                    kind: File,
575                },
576            },
577        }
578        "#);
579    }
580
581    fn test_directory(dirname: &str) {
582        let mock_typeshed = mock_typeshed();
583
584        let path = VendoredPath::new(dirname);
585
586        assert!(mock_typeshed.exists(path));
587        assert!(mock_typeshed.read_to_string(path).is_err());
588        let metadata = mock_typeshed.metadata(path).unwrap();
589        assert!(metadata.kind().is_directory());
590    }
591
592    #[test]
593    fn stdlib_dir_no_trailing_slash() {
594        test_directory("stdlib")
595    }
596
597    #[test]
598    fn stdlib_dir_trailing_slash() {
599        test_directory("stdlib/")
600    }
601
602    #[test]
603    fn asyncio_dir_no_trailing_slash() {
604        test_directory("stdlib/asyncio")
605    }
606
607    #[test]
608    fn asyncio_dir_trailing_slash() {
609        test_directory("stdlib/asyncio/")
610    }
611
612    #[test]
613    fn stdlib_dir_parent_components() {
614        test_directory("stdlib/asyncio/../../stdlib")
615    }
616
617    #[test]
618    fn asyncio_dir_odd_components() {
619        test_directory("./stdlib/asyncio/../asyncio/")
620    }
621
622    fn readdir_snapshot(fs: &VendoredFileSystem, path: &str) -> String {
623        let mut paths = fs
624            .read_directory(VendoredPath::new(path))
625            .map(|entry| entry.path().to_string())
626            .collect::<Vec<String>>();
627        paths.sort();
628        paths.join("\n")
629    }
630
631    #[test]
632    fn read_directory_stdlib() {
633        let mock_typeshed = mock_typeshed();
634
635        assert_snapshot!(readdir_snapshot(&mock_typeshed, "stdlib"), @"
636        vendored://stdlib/asyncio/
637        vendored://stdlib/functools.pyi
638        ");
639        assert_snapshot!(readdir_snapshot(&mock_typeshed, "stdlib/"), @"
640        vendored://stdlib/asyncio/
641        vendored://stdlib/functools.pyi
642        ");
643        assert_snapshot!(readdir_snapshot(&mock_typeshed, "./stdlib"), @"
644        vendored://stdlib/asyncio/
645        vendored://stdlib/functools.pyi
646        ");
647        assert_snapshot!(readdir_snapshot(&mock_typeshed, "./stdlib/"), @"
648        vendored://stdlib/asyncio/
649        vendored://stdlib/functools.pyi
650        ");
651    }
652
653    #[test]
654    fn read_directory_asyncio() {
655        let mock_typeshed = mock_typeshed();
656
657        assert_snapshot!(
658            readdir_snapshot(&mock_typeshed, "stdlib/asyncio"),
659            @"vendored://stdlib/asyncio/tasks.pyi",
660        );
661        assert_snapshot!(
662            readdir_snapshot(&mock_typeshed, "./stdlib/asyncio"),
663            @"vendored://stdlib/asyncio/tasks.pyi",
664        );
665        assert_snapshot!(
666            readdir_snapshot(&mock_typeshed, "stdlib/asyncio/"),
667            @"vendored://stdlib/asyncio/tasks.pyi",
668        );
669        assert_snapshot!(
670            readdir_snapshot(&mock_typeshed, "./stdlib/asyncio/"),
671            @"vendored://stdlib/asyncio/tasks.pyi",
672        );
673    }
674
675    fn test_nonexistent_path(path: &str) {
676        let mock_typeshed = mock_typeshed();
677        let path = VendoredPath::new(path);
678        assert!(!mock_typeshed.exists(path));
679        assert!(mock_typeshed.metadata(path).is_err());
680        assert!(
681            mock_typeshed
682                .read_to_string(path)
683                .is_err_and(|err| err.to_string().contains("file not found"))
684        );
685    }
686
687    #[test]
688    fn simple_nonexistent_path() {
689        test_nonexistent_path("foo")
690    }
691
692    #[test]
693    fn nonexistent_path_with_extension() {
694        test_nonexistent_path("foo.pyi")
695    }
696
697    #[test]
698    fn nonexistent_path_with_trailing_slash() {
699        test_nonexistent_path("foo/")
700    }
701
702    #[test]
703    fn nonexistent_path_with_fancy_components() {
704        test_nonexistent_path("./foo/../../../foo")
705    }
706
707    fn test_file(mock_typeshed: &VendoredFileSystem, path: &VendoredPath) {
708        assert!(mock_typeshed.exists(path));
709        let metadata = mock_typeshed.metadata(path).unwrap();
710        assert!(metadata.kind().is_file());
711    }
712
713    #[test]
714    fn functools_file_contents() {
715        let mock_typeshed = mock_typeshed();
716        let path = VendoredPath::new("stdlib/functools.pyi");
717        test_file(&mock_typeshed, path);
718        let functools_stub = mock_typeshed.read_to_string(path).unwrap();
719        assert_eq!(functools_stub.as_str(), FUNCTOOLS_CONTENTS);
720        // Test that reading the file doesn't leave the archive cursor in the wrong position.
721        let functools_stub_again = mock_typeshed.read_to_string(path).unwrap();
722        assert_eq!(functools_stub_again.as_str(), FUNCTOOLS_CONTENTS);
723    }
724
725    #[test]
726    fn functools_file_other_path() {
727        test_file(
728            &mock_typeshed(),
729            VendoredPath::new("stdlib/../stdlib/../stdlib/functools.pyi"),
730        )
731    }
732
733    #[test]
734    fn asyncio_file_contents() {
735        let mock_typeshed = mock_typeshed();
736        let path = VendoredPath::new("stdlib/asyncio/tasks.pyi");
737        test_file(&mock_typeshed, path);
738        let asyncio_stub = mock_typeshed.read_to_string(path).unwrap();
739        assert_eq!(asyncio_stub.as_str(), ASYNCIO_TASKS_CONTENTS);
740    }
741
742    #[test]
743    fn asyncio_file_other_path() {
744        test_file(
745            &mock_typeshed(),
746            VendoredPath::new("./stdlib/asyncio/../asyncio/tasks.pyi"),
747        )
748    }
749}