1use std::{
5 cmp::{Ordering, min},
6 fmt::{self, Debug, Formatter},
7 io::{self, BufRead, BufReader, Read},
8 path::Path,
9};
10
11use anyhow::{Context, 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, ResolvedVc, ValueToString, Vc};
18use turbo_tasks_hash::{
19 DeterministicHash, DeterministicHasher, HashAlgorithm, deterministic_hash, hash_xxh3_hash64,
20};
21
22use crate::{
23 DiskFileSystem, 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, NonLocalValue, Encode, Decode)]
172pub enum LinkTarget {
173 Absolute {
175 raw: RcStr,
176 resolved: FileSystemPath,
177 },
178 Relative {
179 raw: RcStr,
183 resolved: FileSystemPath,
185 },
186}
187
188impl LinkTarget {
189 pub fn file_system_path(&self) -> &FileSystemPath {
191 match self {
192 LinkTarget::Absolute { resolved, .. } | LinkTarget::Relative { resolved, .. } => {
193 resolved
194 }
195 }
196 }
197
198 pub async fn target_type(&self) -> Result<FileSystemEntryType> {
206 Ok(*self.file_system_path().get_type().await?)
207 }
208
209 pub async fn resolved_type(&self) -> Result<FileSystemEntryType> {
215 match self.file_system_path().realpath().await? {
216 Ok(path) => Ok(*path.get_type().await?),
217 Err(error) => Ok(match error.kind() {
218 RealPathErrorType::NotFound => FileSystemEntryType::NotFound,
219 _ => FileSystemEntryType::Error,
220 }),
221 }
222 }
223}
224
225#[turbo_tasks::value(shared)]
233#[derive(Debug)]
234pub enum LinkContent {
235 Link { target: LinkTarget },
237 NotFound,
242 Invalid { reason: RcStr },
249}
250
251#[derive(Clone, Debug, Hash, PartialEq, Eq, NonLocalValue, DeterministicHash, Encode, Decode)]
253pub enum WriteLinkTargetType {
254 FileNonPortable,
257 DirectoryOrJunctionPoint,
259}
260
261#[turbo_tasks::value(shared)]
268#[derive(Clone, Debug)]
269pub struct WriteLinkContent {
270 pub target: FileSystemPath,
271 pub target_type: WriteLinkTargetType,
272}
273
274impl WriteLinkContent {
275 pub async fn hash(&self, salt: &RcStr, algorithm: HashAlgorithm) -> Result<RcStr> {
278 let target_fs = ResolvedVc::try_downcast_type::<DiskFileSystem>(self.target.fs)
282 .context("link target must use a disk filesystem")?
283 .await?;
284 let target_fs_name = target_fs.name();
285
286 Ok(RcStr::from(deterministic_hash(
287 salt,
288 (target_fs_name, &self.target.path, &self.target_type),
289 algorithm,
290 )))
291 }
292}
293
294#[turbo_tasks::value(shared)]
295#[derive(Clone, DeterministicHash, PartialOrd, Ord)]
296pub struct File {
297 #[turbo_tasks(debug_ignore)]
298 content: Rope,
299 pub(crate) meta: FileMeta,
300}
301
302impl File {
303 pub(crate) fn from_path(p: &Path) -> io::Result<Self> {
305 let mut file = std::fs::File::open(p)?;
306 let metadata = file.metadata()?;
307
308 let mut output = Vec::with_capacity(metadata.len() as usize);
309 file.read_to_end(&mut output)?;
310
311 Ok(File {
312 meta: metadata.into(),
313 content: Rope::from(output),
314 })
315 }
316
317 pub(crate) fn from_bytes(content: Vec<u8>) -> Self {
319 File {
320 meta: FileMeta::default(),
321 content: Rope::from(content),
322 }
323 }
324
325 fn from_rope(content: Rope) -> Self {
327 File {
328 meta: FileMeta::default(),
329 content,
330 }
331 }
332
333 pub fn content_type(&self) -> Option<&Mime> {
335 self.meta.content_type.as_ref()
336 }
337
338 pub fn with_content_type(mut self, content_type: Mime) -> Self {
340 self.meta.content_type = Some(content_type);
341 self
342 }
343
344 pub fn read(&self) -> RopeReader<'_> {
346 self.content.read()
347 }
348}
349
350impl Debug for File {
351 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
352 f.debug_struct("File")
353 .field("meta", &self.meta)
354 .field("content (hash)", &hash_xxh3_hash64(&self.content))
355 .finish()
356 }
357}
358
359impl From<RcStr> for File {
360 fn from(s: RcStr) -> Self {
361 s.into_owned().into()
362 }
363}
364
365impl From<String> for File {
366 fn from(s: String) -> Self {
367 File::from_bytes(s.into_bytes())
368 }
369}
370
371impl From<ReadRef<RcStr>> for File {
372 fn from(s: ReadRef<RcStr>) -> Self {
373 File::from_bytes(s.as_bytes().to_vec())
374 }
375}
376
377impl From<&str> for File {
378 fn from(s: &str) -> Self {
379 File::from_bytes(s.as_bytes().to_vec())
380 }
381}
382
383impl From<Vec<u8>> for File {
384 fn from(bytes: Vec<u8>) -> Self {
385 File::from_bytes(bytes)
386 }
387}
388
389impl From<&[u8]> for File {
390 fn from(bytes: &[u8]) -> Self {
391 File::from_bytes(bytes.to_vec())
392 }
393}
394
395impl From<ReadRef<Rope>> for File {
396 fn from(rope: ReadRef<Rope>) -> Self {
397 File::from_rope(ReadRef::into_owned(rope))
398 }
399}
400
401impl From<Rope> for File {
402 fn from(rope: Rope) -> Self {
403 File::from_rope(rope)
404 }
405}
406
407impl File {
408 pub fn new(meta: FileMeta, content: Vec<u8>) -> Self {
409 Self {
410 meta,
411 content: Rope::from(content),
412 }
413 }
414
415 pub fn meta(&self) -> &FileMeta {
417 &self.meta
418 }
419
420 pub fn content(&self) -> &Rope {
422 &self.content
423 }
424}
425
426#[turbo_tasks::value(shared)]
427#[derive(Debug, Clone, Default)]
428pub struct FileMeta {
429 pub(crate) permissions: Permissions,
432 #[turbo_tasks(unsafe_ignore)]
433 #[bincode(with = "turbo_bincode::mime_option")]
434 content_type: Option<Mime>,
435}
436
437impl Ord for FileMeta {
438 fn cmp(&self, other: &Self) -> Ordering {
439 self.permissions
440 .cmp(&other.permissions)
441 .then_with(|| self.content_type.as_ref().cmp(&other.content_type.as_ref()))
442 }
443}
444
445impl PartialOrd for FileMeta {
446 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
447 Some(self.cmp(other))
448 }
449}
450
451impl From<std::fs::Metadata> for FileMeta {
452 fn from(meta: std::fs::Metadata) -> Self {
453 let permissions = meta.permissions().into();
454
455 Self {
456 permissions,
457 content_type: None,
458 }
459 }
460}
461
462impl DeterministicHash for FileMeta {
463 fn deterministic_hash<H: DeterministicHasher>(&self, state: &mut H) {
464 self.permissions.deterministic_hash(state);
465 if let Some(content_type) = &self.content_type {
466 content_type.to_string().deterministic_hash(state);
467 }
468 }
469}
470
471impl FileContent {
472 pub fn new(file: File) -> Self {
473 FileContent::Content(file)
474 }
475
476 pub fn is_content(&self) -> bool {
477 matches!(self, FileContent::Content(_))
478 }
479
480 pub fn as_content(&self) -> Option<&File> {
481 match self {
482 FileContent::Content(file) => Some(file),
483 FileContent::NotFound => None,
484 }
485 }
486
487 pub fn parse_json_ref(&self) -> FileJsonContent {
488 match self {
489 FileContent::Content(file) => {
490 let content = file.content.clone().into_bytes();
491 let de = &mut serde_json::Deserializer::from_slice(&content);
492 match serde_path_to_error::deserialize(de) {
493 Ok(data) => FileJsonContent::Content(data),
494 Err(e) => FileJsonContent::Unparsable(Box::new(
495 UnparsableJson::from_serde_path_to_error(e),
496 )),
497 }
498 }
499 FileContent::NotFound => FileJsonContent::NotFound,
500 }
501 }
502
503 pub fn parse_json_with_comments_ref(&self) -> FileJsonContent {
504 match self {
505 FileContent::Content(file) => match file.content.to_str() {
506 Ok(string) => match parse_to_serde_value(
507 &string,
508 &ParseOptions {
509 allow_comments: true,
510 allow_trailing_commas: true,
511 allow_loose_object_property_names: false,
512 },
513 ) {
514 Ok(data) => match data {
515 Some(value) => FileJsonContent::Content(value),
516 None => FileJsonContent::unparsable(rcstr!(
517 "text content doesn't contain any json data"
518 )),
519 },
520 Err(e) => FileJsonContent::Unparsable(Box::new(
521 UnparsableJson::from_jsonc_error(e, string.as_ref()),
522 )),
523 },
524 Err(_) => FileJsonContent::unparsable(rcstr!("binary is not valid utf-8 text")),
525 },
526 FileContent::NotFound => FileJsonContent::NotFound,
527 }
528 }
529
530 pub fn parse_json5_ref(&self) -> FileJsonContent {
531 match self {
532 FileContent::Content(file) => match file.content.to_str() {
533 Ok(string) => match parse_to_serde_value(
534 &string,
535 &ParseOptions {
536 allow_comments: true,
537 allow_trailing_commas: true,
538 allow_loose_object_property_names: true,
539 },
540 ) {
541 Ok(data) => match data {
542 Some(value) => FileJsonContent::Content(value),
543 None => FileJsonContent::unparsable(rcstr!(
544 "text content doesn't contain any json data"
545 )),
546 },
547 Err(e) => FileJsonContent::Unparsable(Box::new(
548 UnparsableJson::from_jsonc_error(e, string.as_ref()),
549 )),
550 },
551 Err(_) => FileJsonContent::unparsable(rcstr!("binary is not valid utf-8 text")),
552 },
553 FileContent::NotFound => FileJsonContent::NotFound,
554 }
555 }
556
557 pub fn lines_ref(&self) -> FileLinesContent {
558 match self {
559 FileContent::Content(file) => match file.content.to_str() {
560 Ok(string) => {
561 let mut bytes_offset = 0;
562 FileLinesContent::Lines(
563 string
564 .split('\n')
565 .map(|l| {
566 let line = FileLine {
567 content: l.to_string(),
568 bytes_offset,
569 };
570 bytes_offset += (l.len() + 1) as u32;
571 line
572 })
573 .collect(),
574 )
575 }
576 Err(_) => FileLinesContent::Unparsable,
577 },
578 FileContent::NotFound => FileLinesContent::NotFound,
579 }
580 }
581}
582
583#[turbo_tasks::value_impl]
584impl FileContent {
585 #[turbo_tasks::function]
586 pub fn len(&self) -> Result<Vc<Option<u64>>> {
587 Ok(Vc::cell(match self {
588 FileContent::Content(file) => Some(file.content.len() as u64),
589 FileContent::NotFound => None,
590 }))
591 }
592
593 #[turbo_tasks::function]
594 pub fn parse_json(&self) -> Result<Vc<FileJsonContent>> {
595 Ok(self.parse_json_ref().cell())
596 }
597
598 #[turbo_tasks::function]
599 pub fn parse_json_with_comments(&self) -> Vc<FileJsonContent> {
600 self.parse_json_with_comments_ref().cell()
601 }
602
603 #[turbo_tasks::function]
604 pub fn parse_json5(&self) -> Vc<FileJsonContent> {
605 self.parse_json5_ref().cell()
606 }
607
608 #[turbo_tasks::function]
609 pub fn lines(&self) -> Vc<FileLinesContent> {
610 self.lines_ref().cell()
611 }
612
613 #[turbo_tasks::function]
614 pub async fn hash(&self, salt: Vc<RcStr>, algorithm: HashAlgorithm) -> Result<Vc<RcStr>> {
615 Ok(Vc::cell(RcStr::from(deterministic_hash(
616 &salt.await?,
617 self,
618 algorithm,
619 ))))
620 }
621
622 #[turbo_tasks::function]
627 pub fn persist(&self) -> Vc<PersistedFileContent> {
628 match self {
629 FileContent::Content(file) => PersistedFileContent::Content(file.clone()).cell(),
630 FileContent::NotFound => PersistedFileContent::NotFound.cell(),
631 }
632 }
633
634 #[turbo_tasks::function]
640 pub async fn content_hash(
641 &self,
642 salt: Vc<RcStr>,
643 algorithm: HashAlgorithm,
644 ) -> Result<Vc<Option<RcStr>>> {
645 match self {
646 FileContent::Content(file) => Ok(Vc::cell(Some(
647 deterministic_hash(&salt.await?, file.content().content_hash(), algorithm).into(),
648 ))),
649 FileContent::NotFound => Ok(Vc::cell(None)),
650 }
651 }
652}
653
654#[turbo_tasks::value(shared, serialization = "skip")]
656pub enum FileJsonContent {
657 Content(Value),
658 Unparsable(Box<UnparsableJson>),
659 NotFound,
660}
661
662#[turbo_tasks::value_impl]
663impl ValueToString for FileJsonContent {
664 #[turbo_tasks::function]
669 fn to_string(&self) -> Result<Vc<RcStr>> {
670 match self {
671 FileJsonContent::Content(json) => Ok(Vc::cell(json.to_string().into())),
672 FileJsonContent::Unparsable(e) => bail!("File is not valid JSON: {}", e),
673 FileJsonContent::NotFound => bail!("File not found"),
674 }
675 }
676}
677
678#[turbo_tasks::value_impl]
679impl FileJsonContent {
680 #[turbo_tasks::function]
681 pub async fn content(self: Vc<Self>) -> Result<Vc<Value>> {
682 match &*self.await? {
683 FileJsonContent::Content(json) => Ok(Vc::cell(json.clone())),
684 FileJsonContent::Unparsable(e) => bail!("File is not valid JSON: {}", e),
685 FileJsonContent::NotFound => bail!("File not found"),
686 }
687 }
688}
689impl FileJsonContent {
690 pub fn unparsable(message: RcStr) -> Self {
691 FileJsonContent::Unparsable(Box::new(UnparsableJson {
692 message,
693 path: None,
694 start_location: None,
695 end_location: None,
696 }))
697 }
698
699 pub fn unparsable_with_message(message: RcStr) -> Self {
700 FileJsonContent::Unparsable(Box::new(UnparsableJson {
701 message,
702 path: None,
703 start_location: None,
704 end_location: None,
705 }))
706 }
707}
708
709#[derive(Debug, PartialEq, Eq)]
710pub struct FileLine {
711 pub content: String,
712 pub bytes_offset: u32,
713}
714
715impl FileLine {
716 pub fn len(&self) -> usize {
717 self.content.len()
718 }
719
720 #[must_use]
721 pub fn is_empty(&self) -> bool {
722 self.len() == 0
723 }
724}
725
726#[turbo_tasks::value(shared, serialization = "skip")]
727pub enum FileLinesContent {
728 Lines(#[turbo_tasks(unsafe_ignore)] Vec<FileLine>),
729 Unparsable,
730 NotFound,
731}