1use 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#[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 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#[turbo_tasks::value(shared)]
97#[derive(Clone, Debug, DeterministicHash, PartialOrd, Ord)]
98pub enum PersistedFileContent {
99 Content(File),
100 NotFound,
101}
102
103impl PersistedFileContent {
104 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 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 return Ok(FileComparison::Create);
124 };
125 if new_file.meta != old_meta.into() {
127 return Ok(FileComparison::NotEqual);
128 }
129
130 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#[derive(Clone, Debug, Hash, PartialEq, Eq, TraceRawVcs, NonLocalValue, Encode, Decode)]
172pub enum LinkTarget {
173 Absolute { resolved: FileSystemPath },
175 Relative {
176 raw: RcStr,
180 resolved: FileSystemPath,
182 },
183}
184
185impl LinkTarget {
186 pub fn file_system_path(&self) -> &FileSystemPath {
188 match self {
189 LinkTarget::Absolute { resolved } | LinkTarget::Relative { resolved, .. } => resolved,
190 }
191 }
192
193 pub async fn target_type(&self) -> Result<FileSystemEntryType> {
201 Ok(*self.file_system_path().get_type().await?)
202 }
203
204 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#[turbo_tasks::value(shared)]
228#[derive(Debug)]
229pub enum LinkContent {
230 Link { target: LinkTarget },
232 NotFound,
237 Invalid { reason: RcStr },
244}
245
246#[turbo_tasks::value_impl]
247impl LinkContent {
248 #[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, }
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#[derive(
283 Clone, Debug, Hash, PartialEq, Eq, TraceRawVcs, NonLocalValue, DeterministicHash, Encode, Decode,
284)]
285pub enum WriteLinkTarget {
286 Absolute(RcStr),
288 Relative(RcStr),
290}
291
292#[derive(
294 Clone, Debug, Hash, PartialEq, Eq, TraceRawVcs, NonLocalValue, DeterministicHash, Encode, Decode,
295)]
296pub enum WriteLinkTargetType {
297 FileNonPortable,
300 DirectoryOrJunctionPoint,
302}
303
304#[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 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 pub(crate) fn from_bytes(content: Vec<u8>) -> Self {
342 File {
343 meta: FileMeta::default(),
344 content: Rope::from(content),
345 }
346 }
347
348 fn from_rope(content: Rope) -> Self {
350 File {
351 meta: FileMeta::default(),
352 content,
353 }
354 }
355
356 pub fn content_type(&self) -> Option<&Mime> {
358 self.meta.content_type.as_ref()
359 }
360
361 pub fn with_content_type(mut self, content_type: Mime) -> Self {
363 self.meta.content_type = Some(content_type);
364 self
365 }
366
367 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 pub fn meta(&self) -> &FileMeta {
440 &self.meta
441 }
442
443 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 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 #[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 #[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#[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 #[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}