1use std::{collections::BTreeMap, fmt};
8
9#[derive(Debug, Clone, Copy)]
12pub struct ResourceRequest<'a> {
13 pub target: &'a str,
14 pub referrer: Option<&'a str>,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Resource {
19 pub name: String,
22 pub text: String,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct IncludeResource {
33 pub resource: Resource,
34 pub complete: bool,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct OmittedSourceLine {
44 pub offset: usize,
45 pub line: usize,
46 pub bytes: Vec<u8>,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub struct OmittedSourceSpan {
53 pub line: usize,
54 pub start: usize,
55 pub end: usize,
56}
57
58pub(crate) struct IncludeSnapshot {
59 pub decoded: IncludeResource,
60 pub omissions: Vec<OmittedSourceLine>,
61 pub frames: Option<SourceLineFrames>,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
67pub(crate) struct SourceLineFrames {
68 pub ranges: Vec<(usize, usize)>,
69 pub trailing_lf: bool,
70 pub normalized: bool,
74}
75
76impl From<IncludeResource> for IncludeSnapshot {
77 fn from(decoded: IncludeResource) -> Self {
78 Self {
79 decoded,
80 omissions: Vec::new(),
81 frames: None,
82 }
83 }
84}
85
86impl From<Resource> for IncludeSnapshot {
87 fn from(resource: Resource) -> Self {
88 IncludeResource::from(resource).into()
89 }
90}
91
92pub(crate) fn snapshot_lines(
96 text: &str,
97 omissions: &[OmittedSourceLine],
98) -> Vec<crate::include_indent::Line> {
99 framed_snapshot_lines(text, omissions, None)
100}
101
102pub(crate) fn framed_snapshot_lines(
103 text: &str,
104 omissions: &[OmittedSourceLine],
105 frames: Option<&SourceLineFrames>,
106) -> Vec<crate::include_indent::Line> {
107 use crate::include_indent::Line;
108 let strip_bom = omissions.first().is_none_or(|line| line.line != 1)
109 && !frames.is_some_and(|frames| frames.normalized);
110 let lines = if let Some(frames) = frames {
111 frames
112 .ranges
113 .iter()
114 .enumerate()
115 .map(|(index, &(start, end))| {
116 Line::from_bounds(text, start, end, strip_bom && index == 0)
117 })
118 .collect()
119 } else {
120 Line::from_source_with_bom(text, strip_bom)
121 };
122 if omissions.is_empty() {
123 return lines;
124 }
125 let count = lines.len() + omissions.len();
126 let mut lines = lines.into_iter();
127 let mut omitted = omissions.iter().peekable();
128 (1..=count)
129 .map(|number| {
130 if omitted.peek().is_some_and(|line| line.line == number) {
131 let omission = omitted.next().expect("omitted line");
132 let mut line = Line::generated(String::new());
133 line.original = Some(omission.offset);
134 line
135 } else {
136 lines.next().expect("retained physical line")
137 }
138 })
139 .collect()
140}
141
142#[derive(Debug, Clone)]
145pub enum IncludeInput {
146 Decoded(IncludeResource),
147 Deferred {
148 resource: BinaryResource,
149 binary: bool,
150 },
151 Unconverted {
155 resource: BinaryResource,
156 encoding: Option<String>,
157 },
158}
159
160impl From<Resource> for IncludeResource {
161 fn from(resource: Resource) -> Self {
162 Self {
163 resource,
164 complete: true,
165 }
166 }
167}
168
169#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct BinaryResource {
173 pub name: String,
174 pub bytes: Vec<u8>,
175 pub content_type: Option<String>,
176}
177
178pub(crate) fn load_source_text(
183 provider: &dyn ResourceProvider,
184 request: ResourceRequest<'_>,
185) -> ResourceResult<Resource> {
186 provider
187 .load_asset_text(request)
188 .or_else(|error| match error.kind {
189 ResourceErrorKind::Unsupported => provider.load(request),
190 _ => Err(error),
191 })
192}
193
194pub trait ResourceProvider {
195 fn enable_uri_cache(&self) {}
201
202 fn load_asset_text(&self, request: ResourceRequest<'_>) -> ResourceResult<Resource> {
208 self.load_binary(request)?.decode(None)
209 }
210
211 fn load(&self, request: ResourceRequest<'_>) -> ResourceResult<Resource>;
215
216 fn load_with_encoding(
224 &self,
225 request: ResourceRequest<'_>,
226 encoding: Option<&str>,
227 ) -> ResourceResult<Resource> {
228 let resource = self.load(request)?;
229 if encoding.is_none() {
230 return Ok(resource);
231 }
232 BinaryResource {
233 name: resource.name,
234 bytes: resource.text.into_bytes(),
235 content_type: None,
236 }
237 .decode(encoding)
238 }
239
240 fn load_include(
248 &self,
249 request: ResourceRequest<'_>,
250 encoding: Option<&str>,
251 line_limit: Option<usize>,
252 ) -> ResourceResult<IncludeResource> {
253 let _ = line_limit;
254 self.load_with_encoding(request, encoding).map(Into::into)
255 }
256
257 fn prepare_include(
262 &self,
263 request: ResourceRequest<'_>,
264 encoding: Option<&str>,
265 line_limit: Option<usize>,
266 ) -> ResourceResult<IncludeInput> {
267 self.load_include(request, encoding, line_limit)
268 .map(IncludeInput::Decoded)
269 }
270
271 fn document_path(&self, _name: &str) -> Option<String> {
277 None
278 }
279
280 fn load_binary(&self, request: ResourceRequest<'_>) -> ResourceResult<BinaryResource> {
283 Err(ResourceError::new(
284 ResourceErrorKind::Unsupported,
285 request.target,
286 "binary resources are not supported",
287 ))
288 }
289}
290
291#[derive(Debug, Clone, Copy, PartialEq, Eq)]
292#[non_exhaustive]
293pub enum ResourceErrorKind {
294 NotFound,
295 AccessDenied,
296 InvalidTarget,
297 InvalidEncoding,
298 Io,
299 Unsupported,
301}
302
303#[derive(Debug, Clone, PartialEq, Eq)]
304pub struct ResourceError {
305 pub kind: ResourceErrorKind,
306 pub target: String,
307 pub message: String,
308}
309
310impl ResourceError {
311 pub fn new(
312 kind: ResourceErrorKind,
313 target: impl Into<String>,
314 message: impl Into<String>,
315 ) -> Self {
316 Self {
317 kind,
318 target: target.into(),
319 message: message.into(),
320 }
321 }
322}
323
324impl fmt::Display for ResourceError {
325 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326 write!(f, "{}: {}", self.target, self.message)
327 }
328}
329
330impl std::error::Error for ResourceError {}
331
332pub type ResourceResult<T> = std::result::Result<T, ResourceError>;
333
334#[derive(Debug, Clone, Default)]
339pub struct MemoryResources {
340 files: BTreeMap<String, String>,
341 binaries: BTreeMap<String, Vec<u8>>,
342}
343
344impl MemoryResources {
345 pub fn new() -> Self {
346 Self::default()
347 }
348
349 pub fn insert(
351 &mut self,
352 name: &str,
353 text: impl Into<String>,
354 ) -> ResourceResult<Option<String>> {
355 let name = virtual_name(ResourceRequest {
356 target: name,
357 referrer: None,
358 })?;
359 Ok(self.files.insert(name, text.into()))
360 }
361
362 pub fn insert_binary(
366 &mut self,
367 name: &str,
368 bytes: impl Into<Vec<u8>>,
369 ) -> ResourceResult<Option<Vec<u8>>> {
370 let name = virtual_name(ResourceRequest {
371 target: name,
372 referrer: None,
373 })?;
374 Ok(self.binaries.insert(name, bytes.into()))
375 }
376
377 pub fn remove_binary(&mut self, name: &str) -> ResourceResult<Option<Vec<u8>>> {
378 let name = virtual_name(ResourceRequest {
379 target: name,
380 referrer: None,
381 })?;
382 Ok(self.binaries.remove(&name))
383 }
384
385 pub fn remove(&mut self, name: &str) -> ResourceResult<Option<String>> {
386 let name = virtual_name(ResourceRequest {
387 target: name,
388 referrer: None,
389 })?;
390 Ok(self.files.remove(&name))
391 }
392}
393
394impl ResourceProvider for MemoryResources {
395 fn prepare_include(
396 &self,
397 request: ResourceRequest<'_>,
398 encoding: Option<&str>,
399 line_limit: Option<usize>,
400 ) -> ResourceResult<IncludeInput> {
401 self.load_binary(request)?
402 .prepare_include(encoding, line_limit)
403 }
404
405 fn load_include(
406 &self,
407 request: ResourceRequest<'_>,
408 encoding: Option<&str>,
409 line_limit: Option<usize>,
410 ) -> ResourceResult<IncludeResource> {
411 self.load_binary(request)?
412 .decode_include(encoding, line_limit)
413 }
414
415 fn load_with_encoding(
416 &self,
417 request: ResourceRequest<'_>,
418 encoding: Option<&str>,
419 ) -> ResourceResult<Resource> {
420 self.load_binary(request)?.decode(encoding)
421 }
422
423 fn document_path(&self, name: &str) -> Option<String> {
424 Some(name.to_owned())
425 }
426
427 fn load_binary(&self, request: ResourceRequest<'_>) -> ResourceResult<BinaryResource> {
428 let name = virtual_name(request)?;
429 let bytes = match (self.binaries.get(&name), self.files.get(&name)) {
430 (Some(bytes), _) => bytes.clone(),
431 (None, Some(text)) => text.clone().into_bytes(),
432 (None, None) => {
433 return Err(ResourceError::new(
434 ResourceErrorKind::NotFound,
435 name,
436 "resource not found",
437 ))
438 }
439 };
440 Ok(BinaryResource {
441 name,
442 bytes,
443 content_type: None,
444 })
445 }
446
447 fn load(&self, request: ResourceRequest<'_>) -> ResourceResult<Resource> {
448 let name = virtual_name(request)?;
449 match self.files.get(&name) {
450 Some(text) => Ok(Resource {
451 name,
452 text: text.clone(),
453 }),
454 None => Err(ResourceError::new(
455 ResourceErrorKind::NotFound,
456 name,
457 "resource not found",
458 )),
459 }
460 }
461}
462
463fn virtual_name(request: ResourceRequest<'_>) -> ResourceResult<String> {
464 let target = request.target;
465 if target.is_empty() || target.ends_with('/') || target.contains(['\\', '\0', ':']) {
467 return Err(ResourceError::new(
468 ResourceErrorKind::InvalidTarget,
469 target,
470 "expected a virtual file path using '/' separators",
471 ));
472 }
473 let mut parts = Vec::new();
474 if !target.starts_with('/') {
475 if let Some(referrer) = request.referrer {
476 let parent = referrer.rsplit_once('/').map_or("", |(parent, _)| parent);
477 parts.extend(parent.split('/').filter(|part| !part.is_empty()));
478 }
479 }
480 for part in target.split('/') {
481 match part {
482 "" | "." => {}
483 ".." => {
484 if parts.pop().is_none() {
485 return Err(ResourceError::new(
486 ResourceErrorKind::AccessDenied,
487 target,
488 "path escapes the virtual root",
489 ));
490 }
491 }
492 part => parts.push(part),
493 }
494 }
495 if parts.is_empty() || matches!(target.rsplit('/').next(), Some("." | "..")) {
496 return Err(ResourceError::new(
497 ResourceErrorKind::InvalidTarget,
498 target,
499 "expected a file name",
500 ));
501 }
502 Ok(format!("/{}", parts.join("/")))
503}