Skip to main content

turbo_tasks_fs/
content.rs

1//! File and directory content value types: [`FileContent`], [`File`], JSON and
2//! line views, and link/permission metadata.
3
4use std::{
5    cmp::{Ordering, min},
6    fmt::{self, Debug, Formatter},
7    io::{self, BufRead, BufReader, Read},
8    path::Path,
9};
10
11use anyhow::{Result, bail};
12use bincode::{Decode, Encode};
13use jsonc_parser::{ParseOptions, parse_to_serde_value};
14use mime::Mime;
15use serde_json::Value;
16use turbo_rcstr::{RcStr, rcstr};
17use turbo_tasks::{NonLocalValue, ReadRef, ValueToString, Vc, trace::TraceRawVcs};
18use turbo_tasks_hash::{
19    DeterministicHash, DeterministicHasher, HashAlgorithm, deterministic_hash, hash_xxh3_hash64,
20};
21
22use crate::{
23    FileSystemEntryType, FileSystemPath, RealPathErrorType,
24    json::UnparsableJson,
25    retry::retry_blocking,
26    rope::{Rope, RopeReader},
27    util::extract_disk_access,
28};
29
30#[derive(Clone, Copy, Debug, Default, DeterministicHash, PartialOrd, Ord)]
31#[turbo_tasks::value(shared)]
32pub enum Permissions {
33    Readable,
34    #[default]
35    Writable,
36    Executable,
37}
38
39// Only handle the permissions on unix platform for now
40
41#[cfg(unix)]
42impl From<Permissions> for std::fs::Permissions {
43    fn from(perm: Permissions) -> Self {
44        use std::os::unix::fs::PermissionsExt;
45        match perm {
46            Permissions::Readable => std::fs::Permissions::from_mode(0o444),
47            Permissions::Writable => std::fs::Permissions::from_mode(0o664),
48            Permissions::Executable => std::fs::Permissions::from_mode(0o755),
49        }
50    }
51}
52
53#[cfg(unix)]
54impl From<std::fs::Permissions> for Permissions {
55    fn from(perm: std::fs::Permissions) -> Self {
56        use std::os::unix::fs::PermissionsExt;
57        if perm.readonly() {
58            Permissions::Readable
59        } else {
60            // https://github.com/fitzgen/is_executable/blob/master/src/lib.rs#L96
61            if perm.mode() & 0o111 != 0 {
62                Permissions::Executable
63            } else {
64                Permissions::Writable
65            }
66        }
67    }
68}
69
70#[cfg(not(unix))]
71impl From<std::fs::Permissions> for Permissions {
72    fn from(_: std::fs::Permissions) -> Self {
73        Permissions::default()
74    }
75}
76
77#[turbo_tasks::value(shared, serialization = "hash")]
78#[derive(Clone, Debug, PartialOrd, Ord)]
79pub enum FileContent {
80    Content(File),
81    NotFound,
82}
83
84impl From<File> for FileContent {
85    fn from(file: File) -> Self {
86        FileContent::Content(file)
87    }
88}
89
90/// A persisted version of [`FileContent`] that stores the full file content in the task cache.
91///
92/// [`FileContent`] uses `serialization = "hash"`, so only a hash is kept in the persistent cache.
93/// When reading the file content back from the cache, the hash is compared to detect changes, but
94/// the actual data is not available. `PersistedFileContent` provides the full data so that
95/// [`DiskFileSystem::write`] can retrieve it without re-reading from disk.
96#[turbo_tasks::value(shared)]
97#[derive(Clone, Debug, DeterministicHash, PartialOrd, Ord)]
98pub enum PersistedFileContent {
99    Content(File),
100    NotFound,
101}
102
103impl PersistedFileContent {
104    /// Performs a comparison of self's data against a disk file's streamed read.
105    pub(crate) async fn streaming_compare(&self, path: &Path) -> Result<FileComparison> {
106        let old_file =
107            extract_disk_access(retry_blocking(|| std::fs::File::open(path)).await, path)?;
108        let Some(old_file) = old_file else {
109            return Ok(match self {
110                PersistedFileContent::NotFound => FileComparison::Equal,
111                _ => FileComparison::Create,
112            });
113        };
114        // We know old file exists, does the new file?
115        let PersistedFileContent::Content(new_file) = self else {
116            return Ok(FileComparison::NotEqual);
117        };
118
119        let old_meta = extract_disk_access(retry_blocking(|| old_file.metadata()).await, path)?;
120        let Some(old_meta) = old_meta else {
121            // If we failed to get meta, then the old file has been deleted between the
122            // handle open. In which case, we just pretend the file never existed.
123            return Ok(FileComparison::Create);
124        };
125        // If the meta is different, we need to rewrite the file to update it.
126        if new_file.meta != old_meta.into() {
127            return Ok(FileComparison::NotEqual);
128        }
129
130        // So meta matches, and we have a file handle. Let's stream the contents to see
131        // if they match.
132        let mut new_contents = new_file.read();
133        let mut old_contents = BufReader::new(old_file);
134        Ok(loop {
135            let new_chunk = new_contents.fill_buf()?;
136            let Ok(old_chunk) = old_contents.fill_buf() else {
137                break FileComparison::NotEqual;
138            };
139
140            let len = min(new_chunk.len(), old_chunk.len());
141            if len == 0 {
142                if new_chunk.len() == old_chunk.len() {
143                    break FileComparison::Equal;
144                } else {
145                    break FileComparison::NotEqual;
146                }
147            }
148
149            if new_chunk[0..len] != old_chunk[0..len] {
150                break FileComparison::NotEqual;
151            }
152
153            new_contents.consume(len);
154            old_contents.consume(len);
155        })
156    }
157}
158
159#[derive(Clone, Debug, Eq, PartialEq)]
160pub(crate) enum FileComparison {
161    Create,
162    Equal,
163    NotEqual,
164}
165
166/// The target of a symbolic link, as read from a filesystem.
167///
168/// Every variant carries the `resolved` path the link points at, computed once by
169/// [`crate::FileSystem::read_link`], which is also what guarantees the target stays inside the
170/// filesystem root — a link whose target leaves the root is [`LinkContent::Invalid`] instead.
171#[derive(Clone, Debug, Hash, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
172pub enum LinkTarget {
173    /// The link is an absolute path on disk.
174    Absolute { resolved: FileSystemPath },
175    Relative {
176        /// The value read from the link. The path is lexically converted to a [unix-style
177        /// path][turbo_unix_path::sys_to_unix], but it may contain `..` relative to the *directory
178        /// containing the link*.
179        raw: RcStr,
180        /// The link target relative the a `DiskFileSystem` root.
181        resolved: FileSystemPath,
182    },
183}
184
185impl LinkTarget {
186    /// The path this link points at.
187    pub fn file_system_path(&self) -> &FileSystemPath {
188        match self {
189            LinkTarget::Absolute { resolved } | LinkTarget::Relative { resolved, .. } => resolved,
190        }
191    }
192
193    /// The type of the file this link points at.
194    ///
195    /// This only follows a single link: if the target is itself a symbolic link, this returns
196    /// [`FileSystemEntryType::Symlink`]. Use [`FileSystemPath::realpath`] to follow a chain of
197    /// links.
198    ///
199    /// A dangling link returns [`FileSystemEntryType::NotFound`].
200    pub async fn target_type(&self) -> Result<FileSystemEntryType> {
201        Ok(*self.file_system_path().get_type().await?)
202    }
203
204    /// The type of the file this link ultimately points at.
205    ///
206    /// This follows a chain of links. A dangling link returns
207    /// [`FileSystemEntryType::NotFound`], while any other unresolvable link returns
208    /// [`FileSystemEntryType::Error`].
209    pub async fn resolved_type(&self) -> Result<FileSystemEntryType> {
210        match self.file_system_path().realpath().await? {
211            Ok(path) => Ok(*path.get_type().await?),
212            Err(error) => Ok(match error.kind() {
213                RealPathErrorType::NotFound => FileSystemEntryType::NotFound,
214                _ => FileSystemEntryType::Error,
215            }),
216        }
217    }
218}
219
220/// The contents of a symbolic link, as read from a filesystem. On Windows, this may be a junction
221/// point.
222///
223/// We treat symbolic links and junction points on Windows as equivalent when reading.
224///
225/// This describes the link itself and never the type of the file it points at. Use
226/// [`LinkTarget::target_type`] if you need the type of the target.
227#[turbo_tasks::value(shared)]
228#[derive(Debug)]
229pub enum LinkContent {
230    /// A valid symbolic link pointing to `target`.
231    Link { target: LinkTarget },
232    /// The link itself does not exist at the path given to [`FileSystemPath::read_link`].
233    ///
234    /// This says nothing about whether the link's target exists: a dangling link is still
235    /// returned as [`LinkContent::Link`].
236    NotFound,
237    /// The link could not be read.
238    ///
239    /// This includes all I/O errors other than `NotFound`, denied paths, and targets that leave the
240    /// filesystem root. A relative target that steps out of the root and back into it is also
241    /// invalid, since resolving it would need the names of the root's own ancestors, which a
242    /// root-relative path doesn't carry.
243    Invalid { reason: RcStr },
244}
245
246#[turbo_tasks::value_impl]
247impl LinkContent {
248    /// Hashes the link itself (its target and type), not the content of whatever the link points
249    /// at. This mirrors [`FileContent::hash`] and is the right content hash for consumers that
250    /// re-create a symlink as a symlink instead of copying the resolved file.
251    #[turbo_tasks::function]
252    pub async fn hash(&self, salt: Vc<RcStr>, algorithm: HashAlgorithm) -> Result<Vc<RcStr>> {
253        #[derive(DeterministicHash)]
254        enum SimplifiedLinkContent<'a> {
255            Absolute(&'a RcStr),
256            Relative(&'a RcStr),
257            NotFound,
258            Invalid, // the actual error message doesn't matter for this API
259        }
260        let simplified = match self {
261            LinkContent::Link { target } => match target {
262                LinkTarget::Absolute { resolved } => {
263                    SimplifiedLinkContent::Absolute(&resolved.path)
264                }
265                LinkTarget::Relative { raw, resolved: _ } => SimplifiedLinkContent::Relative(raw),
266            },
267            LinkContent::NotFound => SimplifiedLinkContent::NotFound,
268            LinkContent::Invalid { reason: _ } => SimplifiedLinkContent::Invalid,
269        };
270        Ok(Vc::cell(RcStr::from(deterministic_hash(
271            &salt.await?,
272            simplified,
273            algorithm,
274        ))))
275    }
276}
277
278/// The target of a symbolic link to create, used by [`WriteLinkContent`].
279///
280/// Unlike [`LinkTarget`] this carries only the raw path: the write side never needs the target
281/// resolved, and the link being created may not even exist yet.
282#[derive(
283    Clone, Debug, Hash, PartialEq, Eq, TraceRawVcs, NonLocalValue, DeterministicHash, Encode, Decode,
284)]
285pub enum WriteLinkTarget {
286    /// Normalized and relative to the *filesystem root*.
287    Absolute(RcStr),
288    /// Written verbatim, relative to the *directory containing the link*.
289    Relative(RcStr),
290}
291
292/// The file type of the target of a newly written link. This value is only used on Windows.
293#[derive(
294    Clone, Debug, Hash, PartialEq, Eq, TraceRawVcs, NonLocalValue, DeterministicHash, Encode, Decode,
295)]
296pub enum WriteLinkTargetType {
297    /// Represents a link to a file or a symbolic link that is not a junction point. This is likely
298    /// to fail on Windows, where symbolic links are not enabled by default.
299    FileNonPortable,
300    /// Represents a link to a directory. On Windows, this may also be a link to a junction point.
301    DirectoryOrJunctionPoint,
302}
303
304/// The symbolic link to create at a path, passed to [`FileSystemPath::write_link`].
305///
306/// This is separate from [`LinkContent`] because writing needs to know whether the target is a
307/// directory, while reading a link does not: on Windows we always create junction points for
308/// directories, because symlink creation may fail if "developer mode" is not enabled and we're
309/// running in an unprivileged environment.
310#[turbo_tasks::value(shared)]
311#[derive(Clone, Debug, DeterministicHash)]
312pub struct WriteLinkContent {
313    pub target: WriteLinkTarget,
314    pub target_type: WriteLinkTargetType,
315}
316
317#[turbo_tasks::value(shared)]
318#[derive(Clone, DeterministicHash, PartialOrd, Ord)]
319pub struct File {
320    #[turbo_tasks(debug_ignore)]
321    content: Rope,
322    pub(crate) meta: FileMeta,
323}
324
325impl File {
326    /// Reads a [File] from the given path
327    pub(crate) fn from_path(p: &Path) -> io::Result<Self> {
328        let mut file = std::fs::File::open(p)?;
329        let metadata = file.metadata()?;
330
331        let mut output = Vec::with_capacity(metadata.len() as usize);
332        file.read_to_end(&mut output)?;
333
334        Ok(File {
335            meta: metadata.into(),
336            content: Rope::from(output),
337        })
338    }
339
340    /// Creates a [File] from raw bytes.
341    pub(crate) fn from_bytes(content: Vec<u8>) -> Self {
342        File {
343            meta: FileMeta::default(),
344            content: Rope::from(content),
345        }
346    }
347
348    /// Creates a [File] from a rope.
349    fn from_rope(content: Rope) -> Self {
350        File {
351            meta: FileMeta::default(),
352            content,
353        }
354    }
355
356    /// Returns the content type associated with this file.
357    pub fn content_type(&self) -> Option<&Mime> {
358        self.meta.content_type.as_ref()
359    }
360
361    /// Sets the content type associated with this file.
362    pub fn with_content_type(mut self, content_type: Mime) -> Self {
363        self.meta.content_type = Some(content_type);
364        self
365    }
366
367    /// Returns a Read/AsyncRead/Stream/Iterator to access the File's contents.
368    pub fn read(&self) -> RopeReader<'_> {
369        self.content.read()
370    }
371}
372
373impl Debug for File {
374    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
375        f.debug_struct("File")
376            .field("meta", &self.meta)
377            .field("content (hash)", &hash_xxh3_hash64(&self.content))
378            .finish()
379    }
380}
381
382impl From<RcStr> for File {
383    fn from(s: RcStr) -> Self {
384        s.into_owned().into()
385    }
386}
387
388impl From<String> for File {
389    fn from(s: String) -> Self {
390        File::from_bytes(s.into_bytes())
391    }
392}
393
394impl From<ReadRef<RcStr>> for File {
395    fn from(s: ReadRef<RcStr>) -> Self {
396        File::from_bytes(s.as_bytes().to_vec())
397    }
398}
399
400impl From<&str> for File {
401    fn from(s: &str) -> Self {
402        File::from_bytes(s.as_bytes().to_vec())
403    }
404}
405
406impl From<Vec<u8>> for File {
407    fn from(bytes: Vec<u8>) -> Self {
408        File::from_bytes(bytes)
409    }
410}
411
412impl From<&[u8]> for File {
413    fn from(bytes: &[u8]) -> Self {
414        File::from_bytes(bytes.to_vec())
415    }
416}
417
418impl From<ReadRef<Rope>> for File {
419    fn from(rope: ReadRef<Rope>) -> Self {
420        File::from_rope(ReadRef::into_owned(rope))
421    }
422}
423
424impl From<Rope> for File {
425    fn from(rope: Rope) -> Self {
426        File::from_rope(rope)
427    }
428}
429
430impl File {
431    pub fn new(meta: FileMeta, content: Vec<u8>) -> Self {
432        Self {
433            meta,
434            content: Rope::from(content),
435        }
436    }
437
438    /// Returns the associated [FileMeta] of this file.
439    pub fn meta(&self) -> &FileMeta {
440        &self.meta
441    }
442
443    /// Returns the immutable contents of this file.
444    pub fn content(&self) -> &Rope {
445        &self.content
446    }
447}
448
449#[turbo_tasks::value(shared)]
450#[derive(Debug, Clone, Default)]
451pub struct FileMeta {
452    // Size of the file
453    // len: u64,
454    pub(crate) permissions: Permissions,
455    #[bincode(with = "turbo_bincode::mime_option")]
456    #[turbo_tasks(trace_ignore)]
457    content_type: Option<Mime>,
458}
459
460impl Ord for FileMeta {
461    fn cmp(&self, other: &Self) -> Ordering {
462        self.permissions
463            .cmp(&other.permissions)
464            .then_with(|| self.content_type.as_ref().cmp(&other.content_type.as_ref()))
465    }
466}
467
468impl PartialOrd for FileMeta {
469    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
470        Some(self.cmp(other))
471    }
472}
473
474impl From<std::fs::Metadata> for FileMeta {
475    fn from(meta: std::fs::Metadata) -> Self {
476        let permissions = meta.permissions().into();
477
478        Self {
479            permissions,
480            content_type: None,
481        }
482    }
483}
484
485impl DeterministicHash for FileMeta {
486    fn deterministic_hash<H: DeterministicHasher>(&self, state: &mut H) {
487        self.permissions.deterministic_hash(state);
488        if let Some(content_type) = &self.content_type {
489            content_type.to_string().deterministic_hash(state);
490        }
491    }
492}
493
494impl FileContent {
495    pub fn new(file: File) -> Self {
496        FileContent::Content(file)
497    }
498
499    pub fn is_content(&self) -> bool {
500        matches!(self, FileContent::Content(_))
501    }
502
503    pub fn as_content(&self) -> Option<&File> {
504        match self {
505            FileContent::Content(file) => Some(file),
506            FileContent::NotFound => None,
507        }
508    }
509
510    pub fn parse_json_ref(&self) -> FileJsonContent {
511        match self {
512            FileContent::Content(file) => {
513                let content = file.content.clone().into_bytes();
514                let de = &mut serde_json::Deserializer::from_slice(&content);
515                match serde_path_to_error::deserialize(de) {
516                    Ok(data) => FileJsonContent::Content(data),
517                    Err(e) => FileJsonContent::Unparsable(Box::new(
518                        UnparsableJson::from_serde_path_to_error(e),
519                    )),
520                }
521            }
522            FileContent::NotFound => FileJsonContent::NotFound,
523        }
524    }
525
526    pub fn parse_json_with_comments_ref(&self) -> FileJsonContent {
527        match self {
528            FileContent::Content(file) => match file.content.to_str() {
529                Ok(string) => match parse_to_serde_value(
530                    &string,
531                    &ParseOptions {
532                        allow_comments: true,
533                        allow_trailing_commas: true,
534                        allow_loose_object_property_names: false,
535                    },
536                ) {
537                    Ok(data) => match data {
538                        Some(value) => FileJsonContent::Content(value),
539                        None => FileJsonContent::unparsable(rcstr!(
540                            "text content doesn't contain any json data"
541                        )),
542                    },
543                    Err(e) => FileJsonContent::Unparsable(Box::new(
544                        UnparsableJson::from_jsonc_error(e, string.as_ref()),
545                    )),
546                },
547                Err(_) => FileJsonContent::unparsable(rcstr!("binary is not valid utf-8 text")),
548            },
549            FileContent::NotFound => FileJsonContent::NotFound,
550        }
551    }
552
553    pub fn parse_json5_ref(&self) -> FileJsonContent {
554        match self {
555            FileContent::Content(file) => match file.content.to_str() {
556                Ok(string) => match parse_to_serde_value(
557                    &string,
558                    &ParseOptions {
559                        allow_comments: true,
560                        allow_trailing_commas: true,
561                        allow_loose_object_property_names: true,
562                    },
563                ) {
564                    Ok(data) => match data {
565                        Some(value) => FileJsonContent::Content(value),
566                        None => FileJsonContent::unparsable(rcstr!(
567                            "text content doesn't contain any json data"
568                        )),
569                    },
570                    Err(e) => FileJsonContent::Unparsable(Box::new(
571                        UnparsableJson::from_jsonc_error(e, string.as_ref()),
572                    )),
573                },
574                Err(_) => FileJsonContent::unparsable(rcstr!("binary is not valid utf-8 text")),
575            },
576            FileContent::NotFound => FileJsonContent::NotFound,
577        }
578    }
579
580    pub fn lines_ref(&self) -> FileLinesContent {
581        match self {
582            FileContent::Content(file) => match file.content.to_str() {
583                Ok(string) => {
584                    let mut bytes_offset = 0;
585                    FileLinesContent::Lines(
586                        string
587                            .split('\n')
588                            .map(|l| {
589                                let line = FileLine {
590                                    content: l.to_string(),
591                                    bytes_offset,
592                                };
593                                bytes_offset += (l.len() + 1) as u32;
594                                line
595                            })
596                            .collect(),
597                    )
598                }
599                Err(_) => FileLinesContent::Unparsable,
600            },
601            FileContent::NotFound => FileLinesContent::NotFound,
602        }
603    }
604}
605
606#[turbo_tasks::value_impl]
607impl FileContent {
608    #[turbo_tasks::function]
609    pub fn len(&self) -> Result<Vc<Option<u64>>> {
610        Ok(Vc::cell(match self {
611            FileContent::Content(file) => Some(file.content.len() as u64),
612            FileContent::NotFound => None,
613        }))
614    }
615
616    #[turbo_tasks::function]
617    pub fn parse_json(&self) -> Result<Vc<FileJsonContent>> {
618        Ok(self.parse_json_ref().cell())
619    }
620
621    #[turbo_tasks::function]
622    pub fn parse_json_with_comments(&self) -> Vc<FileJsonContent> {
623        self.parse_json_with_comments_ref().cell()
624    }
625
626    #[turbo_tasks::function]
627    pub fn parse_json5(&self) -> Vc<FileJsonContent> {
628        self.parse_json5_ref().cell()
629    }
630
631    #[turbo_tasks::function]
632    pub fn lines(&self) -> Vc<FileLinesContent> {
633        self.lines_ref().cell()
634    }
635
636    #[turbo_tasks::function]
637    pub async fn hash(&self, salt: Vc<RcStr>, algorithm: HashAlgorithm) -> Result<Vc<RcStr>> {
638        Ok(Vc::cell(RcStr::from(deterministic_hash(
639            &salt.await?,
640            self,
641            algorithm,
642        ))))
643    }
644
645    /// Converts this [`FileContent`] into a [`PersistedFileContent`] by cloning.
646    ///
647    /// Use this in contexts where the full file content must be serialized to the persistent
648    /// task cache (e.g., in [`DiskFileSystem::write`]).
649    #[turbo_tasks::function]
650    pub fn persist(&self) -> Vc<PersistedFileContent> {
651        match self {
652            FileContent::Content(file) => PersistedFileContent::Content(file.clone()).cell(),
653            FileContent::NotFound => PersistedFileContent::NotFound.cell(),
654        }
655    }
656
657    /// Compared to [FileContent::hash], this hashes only the bytes of the file content and
658    /// nothing else, returning `None` if the file does not exist.
659    ///
660    /// If `salt` is non-empty it is written into the hasher before the file bytes in a single
661    /// pass. An empty salt produces the same result as hashing without a prefix.
662    #[turbo_tasks::function]
663    pub async fn content_hash(
664        &self,
665        salt: Vc<RcStr>,
666        algorithm: HashAlgorithm,
667    ) -> Result<Vc<Option<RcStr>>> {
668        match self {
669            FileContent::Content(file) => Ok(Vc::cell(Some(
670                deterministic_hash(&salt.await?, file.content().content_hash(), algorithm).into(),
671            ))),
672            FileContent::NotFound => Ok(Vc::cell(None)),
673        }
674    }
675}
676
677/// A file's content interpreted as a JSON value.
678#[turbo_tasks::value(shared, serialization = "skip")]
679pub enum FileJsonContent {
680    Content(Value),
681    Unparsable(Box<UnparsableJson>),
682    NotFound,
683}
684
685#[turbo_tasks::value_impl]
686impl ValueToString for FileJsonContent {
687    /// Returns the JSON file content as a UTF-8 string.
688    ///
689    /// This operation will only succeed if the file contents are a valid JSON
690    /// value.
691    #[turbo_tasks::function]
692    fn to_string(&self) -> Result<Vc<RcStr>> {
693        match self {
694            FileJsonContent::Content(json) => Ok(Vc::cell(json.to_string().into())),
695            FileJsonContent::Unparsable(e) => bail!("File is not valid JSON: {}", e),
696            FileJsonContent::NotFound => bail!("File not found"),
697        }
698    }
699}
700
701#[turbo_tasks::value_impl]
702impl FileJsonContent {
703    #[turbo_tasks::function]
704    pub async fn content(self: Vc<Self>) -> Result<Vc<Value>> {
705        match &*self.await? {
706            FileJsonContent::Content(json) => Ok(Vc::cell(json.clone())),
707            FileJsonContent::Unparsable(e) => bail!("File is not valid JSON: {}", e),
708            FileJsonContent::NotFound => bail!("File not found"),
709        }
710    }
711}
712impl FileJsonContent {
713    pub fn unparsable(message: RcStr) -> Self {
714        FileJsonContent::Unparsable(Box::new(UnparsableJson {
715            message,
716            path: None,
717            start_location: None,
718            end_location: None,
719        }))
720    }
721
722    pub fn unparsable_with_message(message: RcStr) -> Self {
723        FileJsonContent::Unparsable(Box::new(UnparsableJson {
724            message,
725            path: None,
726            start_location: None,
727            end_location: None,
728        }))
729    }
730}
731
732#[derive(Debug, PartialEq, Eq)]
733pub struct FileLine {
734    pub content: String,
735    pub bytes_offset: u32,
736}
737
738impl FileLine {
739    pub fn len(&self) -> usize {
740        self.content.len()
741    }
742
743    #[must_use]
744    pub fn is_empty(&self) -> bool {
745        self.len() == 0
746    }
747}
748
749#[turbo_tasks::value(shared, serialization = "skip")]
750pub enum FileLinesContent {
751    Lines(#[turbo_tasks(trace_ignore)] Vec<FileLine>),
752    Unparsable,
753    NotFound,
754}