1use std::collections::{HashMap, VecDeque};
2use std::fmt::{Debug, Formatter};
3use std::fs;
4use std::fs::{File, OpenOptions};
5use std::io::{Read, Seek, SeekFrom, Write};
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8
9use bytes::BufMut;
10use log::warn;
11use serde::Deserialize;
12use thiserror::Error;
13
14pub const DOMAINS_SUBDIR: &str = "domains";
15pub const RESOURCES_SUBDIR: &str = "files";
16pub const TMP_SUBDIR: &str = ".tmp";
17pub const VERSIONS_SUBDIR: &str = "versions";
18pub const DRAFTS_SUBDIR: &str = "drafts";
19pub const ECMA_SUBDIR: &str = "ecma";
20pub const PLUGINS_SUBDIR: &str = "plugins";
21
22pub type Result<T> = std::result::Result<T, VfsErr>;
23
24#[derive(Debug, Error)]
25pub enum VfsErr {
26 #[error("Domain not found - {0}")]
27 Domain(String),
28 #[error("File not found - {0}")]
29 FileNotFound(String),
30 #[error("Schema file not found - {0}")]
31 SchemaFileNotFound(String),
32 #[error("Absolute file paths not supported - {0}")]
33 AbsolutePathNotSupported(String),
34 #[error("Dot paths not supported - {0}")]
35 DotPathsNotSupported(String),
36 #[error("Error parsing JSON - {0}")]
37 JsonErr(serde_json::Error),
38 #[error("IO error - {0}")]
39 Io(std::io::Error),
40 #[error("IO error - {0}")]
41 StripPrefixErr(std::path::StripPrefixError),
42 #[error("IO error - {0}")]
43 Utf8(std::string::FromUtf8Error),
44}
45
46#[derive(Debug, Deserialize)]
47pub struct DomainOptions {
48 pub service_id: i64,
49 pub version: String,
50 pub is_draft: bool,
51}
52
53pub trait Vfs: Sync + Send {
78 fn root(&self) -> &PathBuf;
80 fn resolve(&self, child: &str) -> Result<PathBuf> {
81 let root = self.root();
82 let child_path = Path::new(child);
83 if child_path.is_absolute() {
86 Err(VfsErr::AbsolutePathNotSupported(child.to_owned()))
87 } else if child.contains("./") || child.contains("..") {
88 Err(VfsErr::DotPathsNotSupported(child.to_owned()))
89 } else {
90 let resolved = root.join(child_path);
91 let res_str = resolved.to_string_lossy().to_string();
92 if res_str.starts_with(&root.to_string_lossy().to_string())
95 {
97 Ok(resolved)
98 } else {
99 Err(VfsErr::DotPathsNotSupported(child.to_owned()))
101 }
102 }
103 }
104 fn domain_file(&self, domain: &str) -> Result<PathBuf> {
105 self.resolve(format!("{}/{}", DOMAINS_SUBDIR, domain).as_str())
106 }
107 fn resource_dir(&self, service_id: i64) -> Result<PathBuf> {
108 let dir = self.resolve(format!("{}/{}", service_id, RESOURCES_SUBDIR).as_str())?;
109 fs::create_dir_all(dir.clone()).map_err(VfsErr::Io)?;
110 Ok(dir)
111 }
112 fn plugins_dir(&self, service_id: i64) -> Result<PathBuf> {
113 let dir = self.resolve(format!("{}/{}", service_id, PLUGINS_SUBDIR).as_str())?;
114 fs::create_dir_all(dir.clone()).map_err(VfsErr::Io)?;
115 Ok(dir)
116 }
117 fn tmp_dir(&self, service_id: i64) -> Result<PathBuf> {
118 let dir = self.resolve(format!("{}/{}", service_id, TMP_SUBDIR).as_str())?;
119 fs::create_dir_all(dir.clone()).map_err(VfsErr::Io)?;
120 Ok(dir)
121 }
122 fn resource_file(&self, service_id: i64, name: &str) -> Result<PathBuf> {
123 let mut path = self.resource_dir(service_id)?;
124 path.push(name);
125 Ok(path)
126 }
127 fn schema_file(&self, service_id: i64, is_draft: bool, version: &str, file: &str) -> Result<PathBuf> {
128 self.resolve(format!("{}/{}/{}/{}", service_id, if is_draft { DRAFTS_SUBDIR } else { VERSIONS_SUBDIR }, version, file).as_str())
129 }
130 fn ecma_dir(&self, service_id: i64, is_draft: bool, version: &str) -> Result<PathBuf> {
131 self.resolve(
132 format!(
133 "{}/{}/{}/{}",
134 service_id, if is_draft { DRAFTS_SUBDIR } else { VERSIONS_SUBDIR }, version, ECMA_SUBDIR
135 )
136 .as_str(),
137 )
138 }
139 fn read(&self, file: PathBuf) -> Result<Box<dyn Read + '_>>;
140 fn open_with(&self, file: PathBuf, opts: OpenOptions) -> Result<Box<dyn VfsFile>>;
141 fn read_domain_file(&self, domain: &str) -> Result<DomainOptions> {
142 match self.domain_file(domain) {
143 Ok(file) => {
144 let mut data = vec![];
145 let mut input = self.read(file)?;
146 let mut buffer = [0; 1024];
147 while let Ok(n) = input.read(&mut buffer).map_err(VfsErr::Io) {
148 if n == 0 {
149 break;
150 }
151 data.extend_from_slice(&buffer[0..n]);
152 }
153 Ok(serde_json::from_slice(&data).map_err(VfsErr::JsonErr)?)
154 }
155 Err(e) => Err(e),
156 }
157 }
158 fn read_resource_file(&self, service_id: i64, filename: &str) -> Result<Box<dyn Read + '_>> {
159 match self.resource_file(service_id, filename) {
160 Ok(file) => self.read(file),
161 Err(e) => Err(e),
162 }
163 }
164 fn read_schema_file(&self, service_id: i64, is_draft: bool, version: &str, filename: &str) -> Result<String> {
165 match self.schema_file(service_id, is_draft, version, filename) {
166 Ok(file) => {
167 let mut data = vec![];
168 let mut input = self.read(file)?;
169 let mut buffer = [0; 1024];
170 while let Ok(n) = input.read(&mut buffer).map_err(VfsErr::Io) {
171 if n == 0 {
172 break;
173 }
174 data.extend_from_slice(&buffer[0..n]);
175 }
176 Ok(String::from_utf8(data).map_err(VfsErr::Utf8)?)
177 }
178 Err(e) => Err(e),
179 }
180 }
181 fn read_ecma<'a>(&'a self, service_id: i64, is_draft: bool, version: &str) -> Result<DirStream<'a, Self>> {
182 let dir = self.ecma_dir(service_id, is_draft, version)?;
183 self.dir_stream(dir)
184 }
185 fn dir_stream<'a>(&'a self, dir: PathBuf) -> Result<DirStream<'a, Self>> {
186 if dir.to_string_lossy().contains("..") {
187 warn!("ECMA script path cannot contain '..' i.e. must be absolute, full path");
188 return Err(VfsErr::DotPathsNotSupported(format!(
189 "ECMA script path can't have .. in {}",
190 dir.to_string_lossy()
191 )));
192 }
193 match self.read_dir(&dir) {
194 Ok(read_dir) => {
195 let mut stream: DirStream<'a, Self> = DirStream {
196 base: dir,
197 buf: VecDeque::new(),
198 vfs: self,
199 };
200 stream.buf.push_back(read_dir);
201 Ok(stream)
202 }
203 Err(e) => Err(e),
204 }
205 }
206 fn read_dir(&self, dir: &PathBuf) -> Result<VirtualReadDir>;
207}
208
209pub struct VirtualReadDir {
210 inner: Box<dyn Iterator<Item=PathBuf>>,
211}
212
213impl Iterator for VirtualReadDir {
214 type Item = PathBuf;
215
216 fn next(&mut self) -> Option<Self::Item> {
217 self.inner.next()
218 }
219}
220
221pub struct DirStream<'a, F>
222 where
223 F: Vfs + ?Sized,
224{
225 base: PathBuf,
226 buf: VecDeque<VirtualReadDir>,
227 vfs: &'a F,
228}
229
230impl<'a, F: Vfs> Iterator for DirStream<'a, F> {
231 type Item = Result<(PathBuf, PathBuf)>;
232
233 fn next(&mut self) -> Option<Self::Item> {
234 if let Some(dir) = self.buf.back_mut() {
235 if let Some(path) = dir.next() {
236 if path.to_string_lossy().contains("..") {
242 warn!(
243 "Skipping path {} because it contains '..'",
244 path.to_string_lossy()
245 );
246 return self.next();
247 }
248 if path.is_dir() {
249 match self.vfs.read_dir(&path) {
250 Ok(child) => {
251 self.buf.push_front(child);
252 self.next()
253 }
254 Err(e) => Some(Err(e)),
255 }
256 } else {
257 if path.starts_with(&self.base) {
258 let filename = match path
259 .strip_prefix(&self.base)
260 .map_err(VfsErr::StripPrefixErr)
261 {
262 Ok(p) => p,
263 Err(e) => return Some(Err(e)),
264 };
265 Some(Ok((filename.to_owned(), path)))
266 } else {
267 self.next() }
270 }
271 } else {
272 self.buf.pop_back();
273 self.next()
274 }
275 } else {
276 None
277 }
278 }
279}
280
281#[derive(Clone)]
282pub struct FilesystemVfs {
283 services_dir: PathBuf,
286}
287
288pub trait VfsFile: Read + Write + Seek {
289 fn path(&self) -> PathBuf;
290 fn clone(&self) -> Result<Box<dyn VfsFile>>;
291}
292
293impl dyn VfsFile {
294 pub fn save_to<F>(&self, fs: Arc<BoundVfs<F>>, new_name: Option<String>) -> Result<String>
295 where
296 F: Vfs,
297 {
298 fs.save_to(self, new_name)
299 }
300 pub fn discard<F>(&self, fs: Arc<BoundVfs<F>>) -> Result<()>
301 where
302 F: Vfs,
303 {
304 fs.discard(self)
305 }
306}
307
308impl Debug for dyn VfsFile {
309 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
310 f.write_str("VfsFile")
311 }
312}
313
314pub struct VfsFileSystemFile(File, PathBuf);
315
316impl VfsFile for VfsFileSystemFile {
317 fn path(&self) -> PathBuf {
318 self.1.clone()
319 }
320 fn clone(&self) -> Result<Box<dyn VfsFile>> {
321 let mut opts = OpenOptions::new();
322 opts.read(true);
323 Ok(Box::new(VfsFileSystemFile(
324 opts.open(self.1.clone()).map_err(VfsErr::Io)?,
325 self.1.clone(),
326 )))
327 }
328}
329
330impl Read for VfsFileSystemFile {
331 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
332 self.0.read(buf)
333 }
334}
335
336impl Write for VfsFileSystemFile {
337 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
338 self.0.write(buf)
339 }
340
341 fn flush(&mut self) -> std::io::Result<()> {
342 self.0.flush()
343 }
344}
345
346impl Seek for VfsFileSystemFile {
347 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
348 self.0.seek(pos)
349 }
350}
351
352impl Vfs for FilesystemVfs {
353 fn root(&self) -> &PathBuf {
354 &self.services_dir
355 }
356
357 fn read(&self, file: PathBuf) -> Result<Box<dyn Read + '_>> {
358 if file.to_string_lossy().contains("..") {
359 return Err(VfsErr::DotPathsNotSupported(format!(
360 "Cannot read file with .. in path {}",
361 file.to_string_lossy()
362 )));
363 }
364 Ok(Box::new(File::open(file).map_err(VfsErr::Io)?))
365 }
366 fn open_with(&self, path: PathBuf, opts: OpenOptions) -> Result<Box<dyn VfsFile>> {
367 if path.to_string_lossy().contains("..") {
368 return Err(VfsErr::DotPathsNotSupported(format!(
369 "Cannot open file with .. in path {}",
370 path.to_string_lossy()
371 )));
372 }
373 let file = opts.open(path.clone()).map_err(VfsErr::Io)?;
374 Ok(Box::new(VfsFileSystemFile(file, path)))
375 }
376
377 fn read_dir(&self, dir: &PathBuf) -> Result<VirtualReadDir> {
378 if dir.to_string_lossy().contains("..") {
379 return Err(VfsErr::DotPathsNotSupported(format!(
380 "Cannot read dir with .. in path {}",
381 dir.to_string_lossy()
382 )));
383 }
384 let it = fs::read_dir(dir).map_err(VfsErr::Io)?;
385 let it = it.map(|v| v.map(|e| e.path())).flatten();
386 let it: Box<dyn Iterator<Item=PathBuf>> = Box::new(it);
387 Ok(VirtualReadDir { inner: it })
388 }
389}
390
391impl FilesystemVfs {
392 pub fn new(services_dir: String) -> Self {
393 FilesystemVfs {
394 services_dir: PathBuf::from(services_dir),
395 }
396 }
397}
398
399#[allow(unused)]
400pub struct MemVfsFile {
401 path: PathBuf,
402 data: Vec<u8>,
403 offset: usize,
404}
405
406impl Seek for MemVfsFile {
407 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
408 match pos {
409 SeekFrom::Start(_start) => {}
410 SeekFrom::End(_end) => {}
411 SeekFrom::Current(_current) => {}
412 }
413 todo!();
414 }
416}
417
418impl Read for MemVfsFile {
419 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
420 let start = self.offset;
421 let mut end = start + buf.len();
422 let buf_len = self.data.len();
423 if end >= buf_len {
424 end = buf_len;
425 }
426 if start >= end {
427 return Ok(0);
428 }
429 let slice = &self.data[start..end];
430 let read = end - start;
431 buf[0..read].clone_from_slice(slice);
432 self.offset = end;
433 Ok(read)
434 }
435}
436
437impl Write for MemVfsFile {
438 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
439 self.data.put_slice(buf);
440 Ok(buf.len())
441 }
442
443 fn flush(&mut self) -> std::io::Result<()> {
444 println!(
445 "MemVfsFile::flush:{}",
446 String::from_utf8(self.data.clone()).unwrap()
447 );
448 Ok(())
449 }
450}
451
452impl VfsFile for MemVfsFile {
453 fn path(&self) -> PathBuf {
454 self.path.clone()
455 }
456 fn clone(&self) -> Result<Box<dyn VfsFile>> {
457 Ok(Box::new(MemVfsFile {
458 path: self.path.clone(),
459 data: self.data.clone(),
460 offset: 0,
461 }))
462 }
463}
464
465#[derive(Clone)]
466pub struct MemoryVfs {
467 pub root: PathBuf,
468 pub data: HashMap<String, String>,
469}
470
471impl Vfs for MemoryVfs {
472 fn root(&self) -> &PathBuf {
473 &self.root
474 }
475
476 fn read(&self, file: PathBuf) -> Result<Box<dyn Read + '_>> {
477 if file.to_string_lossy().contains("..") {
478 return Err(VfsErr::DotPathsNotSupported(format!(
479 "Cannot read file with .. in path {}",
480 file.to_string_lossy()
481 )));
482 }
483 match self.data.get(file.to_string_lossy().as_ref()) {
484 Some(data) => {
485 let data: &[u8] = data.as_bytes();
486 Ok(Box::new(data))
487 }
488 None => Err(VfsErr::FileNotFound(format!(
489 "File not found - {}",
490 file.to_string_lossy()
491 ))),
492 }
493 }
494
495 fn open_with(&self, file: PathBuf, _opts: OpenOptions) -> Result<Box<dyn VfsFile>> {
496 if file.to_string_lossy().contains("..") {
497 return Err(VfsErr::DotPathsNotSupported(format!(
498 "Cannot read file with .. in path {}",
499 file.to_string_lossy()
500 )));
501 }
502 match self.data.get(file.to_string_lossy().as_ref()) {
503 Some(data) => {
504 let data: &[u8] = data.as_bytes();
505 Ok(Box::new(MemVfsFile {
506 path: file,
507 data: Vec::from(data),
508 offset: 0,
509 }))
510 }
511 None => {
512 Ok(Box::new(MemVfsFile {
514 path: file,
515 data: vec![],
516 offset: 0,
517 }))
518 }
523 }
524 }
525
526 fn read_dir(&self, dir: &PathBuf) -> Result<VirtualReadDir> {
527 if dir.to_string_lossy().contains("..") {
528 return Err(VfsErr::DotPathsNotSupported(format!(
529 "Cannot read dir with .. in path {}",
530 dir.to_string_lossy()
531 )));
532 }
533 let it: Vec<_> = self
534 .data
535 .keys()
536 .map(PathBuf::from)
537 .skip_while(|path| !path.starts_with(dir))
538 .collect();
539 Ok(VirtualReadDir {
540 inner: Box::new(it.into_iter()),
541 })
542 }
543}
544
545pub struct BoundVfs<F>
546 where
547 F: Vfs,
548{
549 pub options: DomainOptions,
550 pub vfs: Arc<F>,
551}
552
553impl<F> BoundVfs<F>
554 where
555 F: Vfs,
556{
557 pub fn new(options: DomainOptions, vfs: Arc<F>) -> BoundVfs<F> {
558 Self { options, vfs }
559 }
560 pub fn read_schema_file(&self, name: &str) -> Result<String> {
561 self.vfs
562 .read_schema_file(self.options.service_id, self.options.is_draft, self.options.version.as_str(), name)
563 }
564
565 pub fn ecma_files(&self) -> Result<DirStream<F>> {
566 self.vfs
567 .read_ecma(self.options.service_id, self.options.is_draft, self.options.version.as_str())
568 }
569
570 pub fn read_ecma_file(&self, mut file: PathBuf) -> Result<String> {
571 if file.starts_with("./") {
572 file = file
573 .strip_prefix("./")
574 .map_err(VfsErr::StripPrefixErr)?
575 .to_owned();
576 }
577 let mut path = self
578 .vfs
579 .ecma_dir(self.options.service_id, self.options.is_draft, self.options.version.as_str())?;
580 path.push(file);
581 let mut read = self.vfs.read(path)?;
582 let mut str = String::new();
583 read.read_to_string(&mut str).map_err(VfsErr::Io)?;
584 Ok(str)
585 }
586
587 pub fn resource_dir(&self) -> Result<PathBuf> {
588 self.vfs.resource_dir(self.options.service_id)
589 }
590
591 pub fn resolve_resource(&self, mut file: PathBuf) -> Result<PathBuf> {
592 if file.starts_with("./") {
593 file = file
594 .strip_prefix("./")
595 .map_err(VfsErr::StripPrefixErr)?
596 .to_owned();
597 } else if file.to_string_lossy().contains("..") {
598 return Err(VfsErr::DotPathsNotSupported(format!(
599 "Cannot open file with .. in path {}",
600 file.to_string_lossy()
601 )));
602 }
603 let mut path = self.vfs.resource_dir(self.options.service_id)?;
604 path.push(file);
605 Ok(path)
606 }
607 pub fn resolve_plugin(&self, mut file: PathBuf) -> Result<PathBuf> {
608 if file.starts_with("./") {
609 file = file
610 .strip_prefix("./")
611 .map_err(VfsErr::StripPrefixErr)?
612 .to_owned();
613 } else if file.to_string_lossy().contains("..") {
614 return Err(VfsErr::DotPathsNotSupported(format!(
615 "Cannot open file with .. in path {}",
616 file.to_string_lossy()
617 )));
618 }
619 let mut path = self.vfs.plugins_dir(self.options.service_id)?;
620 path.push(file);
621 Ok(path)
622 }
623 pub fn open(&self, mut file: PathBuf, opts: OpenOptions) -> Result<Box<dyn VfsFile>> {
624 if file.starts_with("./") {
625 file = file
626 .strip_prefix("./")
627 .map_err(VfsErr::StripPrefixErr)?
628 .to_owned();
629 } else if file.to_string_lossy().contains("..") {
630 return Err(VfsErr::DotPathsNotSupported(format!(
631 "Cannot open file with .. in path {}",
632 file.to_string_lossy()
633 )));
634 }
635 self.vfs.open_with(self.resolve_resource(file)?, opts)
636 }
637
638 pub fn discard<I>(&self, _file: &I) -> Result<()>
639 where
640 I: VfsFile + ?Sized,
641 {
642 todo!();
643 }
645 pub fn save_to<I>(&self, file: &I, new_name: Option<String>) -> Result<String>
646 where
647 I: VfsFile + ?Sized,
648 {
649 let mut other_path = file.path();
650 let mut path = self.vfs.resource_dir(self.options.service_id)?;
651 if other_path.starts_with(&path) {
652 other_path = PathBuf::from(
653 other_path
654 .strip_prefix(&path)
655 .map_err(VfsErr::StripPrefixErr)?,
656 );
657 }
658 if other_path.starts_with(TMP_SUBDIR) {
659 other_path = PathBuf::from(
660 other_path
661 .strip_prefix(TMP_SUBDIR)
662 .map_err(VfsErr::StripPrefixErr)?,
663 )
664 }
665 path.push(other_path);
666 if let Some(file_name) = new_name {
667 path.set_file_name(file_name);
668 }
669 let name = if let Some(name) = path.file_name().map(|v| v.to_str()).flatten() {
670 name.to_string()
671 } else {
672 file.path()
673 .to_string_lossy()
674 .split("/")
675 .last()
676 .unwrap()
677 .to_string()
678 };
679 fs::rename(file.path(), path).map_err(VfsErr::Io)?;
680 Ok(name)
681 }
682}