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 bitflags::bitflags;
14use jsonc_parser::{ParseOptions, parse_to_serde_value};
15use mime::Mime;
16use serde_json::Value;
17use turbo_rcstr::{RcStr, rcstr};
18use turbo_tasks::{NonLocalValue, ReadRef, ValueToString, Vc, trace::TraceRawVcs};
19use turbo_tasks_hash::{
20 DeterministicHash, DeterministicHasher, HashAlgorithm, deterministic_hash, hash_xxh3_hash64,
21};
22
23use crate::{
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
166bitflags! {
167 #[derive(
168 Default,
169 TraceRawVcs,
170 NonLocalValue,
171 DeterministicHash,
172 Encode,
173 Decode,
174 )]
175 pub struct LinkType: u8 {
176 const DIRECTORY = 0b00000001;
177 const ABSOLUTE = 0b00000010;
178 }
179}
180
181#[turbo_tasks::value(shared)]
187#[derive(Debug, DeterministicHash)]
188pub enum LinkContent {
189 Link {
200 target: RcStr,
201 link_type: LinkType,
202 },
203 Invalid,
205 NotFound,
207}
208
209#[turbo_tasks::value(shared)]
210#[derive(Clone, DeterministicHash, PartialOrd, Ord)]
211pub struct File {
212 #[turbo_tasks(debug_ignore)]
213 content: Rope,
214 pub(crate) meta: FileMeta,
215}
216
217impl File {
218 pub(crate) fn from_path(p: &Path) -> io::Result<Self> {
220 let mut file = std::fs::File::open(p)?;
221 let metadata = file.metadata()?;
222
223 let mut output = Vec::with_capacity(metadata.len() as usize);
224 file.read_to_end(&mut output)?;
225
226 Ok(File {
227 meta: metadata.into(),
228 content: Rope::from(output),
229 })
230 }
231
232 pub(crate) fn from_bytes(content: Vec<u8>) -> Self {
234 File {
235 meta: FileMeta::default(),
236 content: Rope::from(content),
237 }
238 }
239
240 fn from_rope(content: Rope) -> Self {
242 File {
243 meta: FileMeta::default(),
244 content,
245 }
246 }
247
248 pub fn content_type(&self) -> Option<&Mime> {
250 self.meta.content_type.as_ref()
251 }
252
253 pub fn with_content_type(mut self, content_type: Mime) -> Self {
255 self.meta.content_type = Some(content_type);
256 self
257 }
258
259 pub fn read(&self) -> RopeReader<'_> {
261 self.content.read()
262 }
263}
264
265impl Debug for File {
266 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
267 f.debug_struct("File")
268 .field("meta", &self.meta)
269 .field("content (hash)", &hash_xxh3_hash64(&self.content))
270 .finish()
271 }
272}
273
274impl From<RcStr> for File {
275 fn from(s: RcStr) -> Self {
276 s.into_owned().into()
277 }
278}
279
280impl From<String> for File {
281 fn from(s: String) -> Self {
282 File::from_bytes(s.into_bytes())
283 }
284}
285
286impl From<ReadRef<RcStr>> for File {
287 fn from(s: ReadRef<RcStr>) -> Self {
288 File::from_bytes(s.as_bytes().to_vec())
289 }
290}
291
292impl From<&str> for File {
293 fn from(s: &str) -> Self {
294 File::from_bytes(s.as_bytes().to_vec())
295 }
296}
297
298impl From<Vec<u8>> for File {
299 fn from(bytes: Vec<u8>) -> Self {
300 File::from_bytes(bytes)
301 }
302}
303
304impl From<&[u8]> for File {
305 fn from(bytes: &[u8]) -> Self {
306 File::from_bytes(bytes.to_vec())
307 }
308}
309
310impl From<ReadRef<Rope>> for File {
311 fn from(rope: ReadRef<Rope>) -> Self {
312 File::from_rope(ReadRef::into_owned(rope))
313 }
314}
315
316impl From<Rope> for File {
317 fn from(rope: Rope) -> Self {
318 File::from_rope(rope)
319 }
320}
321
322impl File {
323 pub fn new(meta: FileMeta, content: Vec<u8>) -> Self {
324 Self {
325 meta,
326 content: Rope::from(content),
327 }
328 }
329
330 pub fn meta(&self) -> &FileMeta {
332 &self.meta
333 }
334
335 pub fn content(&self) -> &Rope {
337 &self.content
338 }
339}
340
341#[turbo_tasks::value(shared)]
342#[derive(Debug, Clone, Default)]
343pub struct FileMeta {
344 pub(crate) permissions: Permissions,
347 #[bincode(with = "turbo_bincode::mime_option")]
348 #[turbo_tasks(trace_ignore)]
349 content_type: Option<Mime>,
350}
351
352impl Ord for FileMeta {
353 fn cmp(&self, other: &Self) -> Ordering {
354 self.permissions
355 .cmp(&other.permissions)
356 .then_with(|| self.content_type.as_ref().cmp(&other.content_type.as_ref()))
357 }
358}
359
360impl PartialOrd for FileMeta {
361 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
362 Some(self.cmp(other))
363 }
364}
365
366impl From<std::fs::Metadata> for FileMeta {
367 fn from(meta: std::fs::Metadata) -> Self {
368 let permissions = meta.permissions().into();
369
370 Self {
371 permissions,
372 content_type: None,
373 }
374 }
375}
376
377impl DeterministicHash for FileMeta {
378 fn deterministic_hash<H: DeterministicHasher>(&self, state: &mut H) {
379 self.permissions.deterministic_hash(state);
380 if let Some(content_type) = &self.content_type {
381 content_type.to_string().deterministic_hash(state);
382 }
383 }
384}
385
386impl FileContent {
387 pub fn new(file: File) -> Self {
388 FileContent::Content(file)
389 }
390
391 pub fn is_content(&self) -> bool {
392 matches!(self, FileContent::Content(_))
393 }
394
395 pub fn as_content(&self) -> Option<&File> {
396 match self {
397 FileContent::Content(file) => Some(file),
398 FileContent::NotFound => None,
399 }
400 }
401
402 pub fn parse_json_ref(&self) -> FileJsonContent {
403 match self {
404 FileContent::Content(file) => {
405 let content = file.content.clone().into_bytes();
406 let de = &mut serde_json::Deserializer::from_slice(&content);
407 match serde_path_to_error::deserialize(de) {
408 Ok(data) => FileJsonContent::Content(data),
409 Err(e) => FileJsonContent::Unparsable(Box::new(
410 UnparsableJson::from_serde_path_to_error(e),
411 )),
412 }
413 }
414 FileContent::NotFound => FileJsonContent::NotFound,
415 }
416 }
417
418 pub fn parse_json_with_comments_ref(&self) -> FileJsonContent {
419 match self {
420 FileContent::Content(file) => match file.content.to_str() {
421 Ok(string) => match parse_to_serde_value(
422 &string,
423 &ParseOptions {
424 allow_comments: true,
425 allow_trailing_commas: true,
426 allow_loose_object_property_names: false,
427 },
428 ) {
429 Ok(data) => match data {
430 Some(value) => FileJsonContent::Content(value),
431 None => FileJsonContent::unparsable(rcstr!(
432 "text content doesn't contain any json data"
433 )),
434 },
435 Err(e) => FileJsonContent::Unparsable(Box::new(
436 UnparsableJson::from_jsonc_error(e, string.as_ref()),
437 )),
438 },
439 Err(_) => FileJsonContent::unparsable(rcstr!("binary is not valid utf-8 text")),
440 },
441 FileContent::NotFound => FileJsonContent::NotFound,
442 }
443 }
444
445 pub fn parse_json5_ref(&self) -> FileJsonContent {
446 match self {
447 FileContent::Content(file) => match file.content.to_str() {
448 Ok(string) => match parse_to_serde_value(
449 &string,
450 &ParseOptions {
451 allow_comments: true,
452 allow_trailing_commas: true,
453 allow_loose_object_property_names: true,
454 },
455 ) {
456 Ok(data) => match data {
457 Some(value) => FileJsonContent::Content(value),
458 None => FileJsonContent::unparsable(rcstr!(
459 "text content doesn't contain any json data"
460 )),
461 },
462 Err(e) => FileJsonContent::Unparsable(Box::new(
463 UnparsableJson::from_jsonc_error(e, string.as_ref()),
464 )),
465 },
466 Err(_) => FileJsonContent::unparsable(rcstr!("binary is not valid utf-8 text")),
467 },
468 FileContent::NotFound => FileJsonContent::NotFound,
469 }
470 }
471
472 pub fn lines_ref(&self) -> FileLinesContent {
473 match self {
474 FileContent::Content(file) => match file.content.to_str() {
475 Ok(string) => {
476 let mut bytes_offset = 0;
477 FileLinesContent::Lines(
478 string
479 .split('\n')
480 .map(|l| {
481 let line = FileLine {
482 content: l.to_string(),
483 bytes_offset,
484 };
485 bytes_offset += (l.len() + 1) as u32;
486 line
487 })
488 .collect(),
489 )
490 }
491 Err(_) => FileLinesContent::Unparsable,
492 },
493 FileContent::NotFound => FileLinesContent::NotFound,
494 }
495 }
496}
497
498#[turbo_tasks::value_impl]
499impl FileContent {
500 #[turbo_tasks::function]
501 pub fn len(&self) -> Result<Vc<Option<u64>>> {
502 Ok(Vc::cell(match self {
503 FileContent::Content(file) => Some(file.content.len() as u64),
504 FileContent::NotFound => None,
505 }))
506 }
507
508 #[turbo_tasks::function]
509 pub fn parse_json(&self) -> Result<Vc<FileJsonContent>> {
510 Ok(self.parse_json_ref().cell())
511 }
512
513 #[turbo_tasks::function]
514 pub fn parse_json_with_comments(&self) -> Vc<FileJsonContent> {
515 self.parse_json_with_comments_ref().cell()
516 }
517
518 #[turbo_tasks::function]
519 pub fn parse_json5(&self) -> Vc<FileJsonContent> {
520 self.parse_json5_ref().cell()
521 }
522
523 #[turbo_tasks::function]
524 pub fn lines(&self) -> Vc<FileLinesContent> {
525 self.lines_ref().cell()
526 }
527
528 #[turbo_tasks::function]
529 pub async fn hash(&self, salt: Vc<RcStr>, algorithm: HashAlgorithm) -> Result<Vc<RcStr>> {
530 Ok(Vc::cell(RcStr::from(deterministic_hash(
531 &salt.await?,
532 self,
533 algorithm,
534 ))))
535 }
536
537 #[turbo_tasks::function]
542 pub fn persist(&self) -> Vc<PersistedFileContent> {
543 match self {
544 FileContent::Content(file) => PersistedFileContent::Content(file.clone()).cell(),
545 FileContent::NotFound => PersistedFileContent::NotFound.cell(),
546 }
547 }
548
549 #[turbo_tasks::function]
555 pub async fn content_hash(
556 &self,
557 salt: Vc<RcStr>,
558 algorithm: HashAlgorithm,
559 ) -> Result<Vc<Option<RcStr>>> {
560 match self {
561 FileContent::Content(file) => Ok(Vc::cell(Some(
562 deterministic_hash(&salt.await?, file.content().content_hash(), algorithm).into(),
563 ))),
564 FileContent::NotFound => Ok(Vc::cell(None)),
565 }
566 }
567}
568
569#[turbo_tasks::value(shared, serialization = "skip")]
571pub enum FileJsonContent {
572 Content(Value),
573 Unparsable(Box<UnparsableJson>),
574 NotFound,
575}
576
577#[turbo_tasks::value_impl]
578impl ValueToString for FileJsonContent {
579 #[turbo_tasks::function]
584 fn to_string(&self) -> Result<Vc<RcStr>> {
585 match self {
586 FileJsonContent::Content(json) => Ok(Vc::cell(json.to_string().into())),
587 FileJsonContent::Unparsable(e) => bail!("File is not valid JSON: {}", e),
588 FileJsonContent::NotFound => bail!("File not found"),
589 }
590 }
591}
592
593#[turbo_tasks::value_impl]
594impl FileJsonContent {
595 #[turbo_tasks::function]
596 pub async fn content(self: Vc<Self>) -> Result<Vc<Value>> {
597 match &*self.await? {
598 FileJsonContent::Content(json) => Ok(Vc::cell(json.clone())),
599 FileJsonContent::Unparsable(e) => bail!("File is not valid JSON: {}", e),
600 FileJsonContent::NotFound => bail!("File not found"),
601 }
602 }
603}
604impl FileJsonContent {
605 pub fn unparsable(message: RcStr) -> Self {
606 FileJsonContent::Unparsable(Box::new(UnparsableJson {
607 message,
608 path: None,
609 start_location: None,
610 end_location: None,
611 }))
612 }
613
614 pub fn unparsable_with_message(message: RcStr) -> Self {
615 FileJsonContent::Unparsable(Box::new(UnparsableJson {
616 message,
617 path: None,
618 start_location: None,
619 end_location: None,
620 }))
621 }
622}
623
624#[derive(Debug, PartialEq, Eq)]
625pub struct FileLine {
626 pub content: String,
627 pub bytes_offset: u32,
628}
629
630impl FileLine {
631 pub fn len(&self) -> usize {
632 self.content.len()
633 }
634
635 #[must_use]
636 pub fn is_empty(&self) -> bool {
637 self.len() == 0
638 }
639}
640
641#[turbo_tasks::value(shared, serialization = "skip")]
642pub enum FileLinesContent {
643 Lines(#[turbo_tasks(trace_ignore)] Vec<FileLine>),
644 Unparsable,
645 NotFound,
646}