1use std::collections::{BTreeMap, BTreeSet, HashMap};
16
17use bynk_syntax::ast::{CommonsItem, EmbedDecl, FnName, Ident};
18use bynk_syntax::error::CompileError;
19
20use crate::project_model::{ErrorSink, UnitInfo};
21use bynk_project::ParsedFile;
22
23pub fn unsafe_path_error(e: &EmbedDecl) -> Option<CompileError> {
28 let reason = crate::assets::unsafe_asset_path_reason(&e.path)?;
29 Some(
30 CompileError::new(
31 "bynk.embed.unsafe_path",
32 e.path_span,
33 format!("embed `{}` has an unsafe path: {reason}", e.name.name),
34 )
35 .with_note("an embed path is relative to the project root and must stay inside it"),
36 )
37}
38
39pub fn outside_embeds_dir_error(e: &EmbedDecl) -> Option<CompileError> {
45 if crate::assets::unsafe_asset_path_reason(&e.path).is_some()
46 || bynk_project::assets::is_in_embeds_dir(&e.path)
47 {
48 return None;
49 }
50 let dir = bynk_project::assets::PROJECT_EMBEDS_DIR;
51 Some(
52 CompileError::new(
53 "bynk.embed.outside_embeds_dir",
54 e.path_span,
55 format!(
56 "embed `{}` must name a file in the project's `{dir}/` folder",
57 e.name.name
58 ),
59 )
60 .with_note(format!(
61 "an embed is compiled into the deployed code, so it comes only from `{dir}/`; move the file there and name it `{dir}/…`"
62 )),
63 )
64}
65
66pub fn path_errors(e: &EmbedDecl) -> Option<CompileError> {
69 unsafe_path_error(e).or_else(|| outside_embeds_dir_error(e))
70}
71
72pub fn placement_error(e: &EmbedDecl) -> CompileError {
74 CompileError::new(
75 "bynk.embed.outside_context_or_commons",
76 e.span,
77 "`embed` declarations are only allowed inside a context or a commons",
78 )
79 .with_note(
80 "an embed is compiled into the Worker of each context that declares it or `uses` a commons that does",
81 )
82}
83
84pub const MAX_EMBED_BYTES: usize = 256 * 1024;
88
89pub const MAX_EMBEDS_TOTAL_BYTES: usize = 1024 * 1024;
92
93pub fn needs_project_error(e: &EmbedDecl) -> CompileError {
98 CompileError::new(
99 "bynk.embed.needs_project",
100 e.span,
101 format!(
102 "embed `{}` cannot be built from a single file",
103 e.name.name
104 ),
105 )
106 .with_note(
107 "an embed reads a file from the project's `embeds/` folder, so it needs a project build (`bynk dev`, `bynkc compile <project dir>`)",
108 )
109}
110
111pub fn embedded_text(bytes: &[u8]) -> Result<String, &'static str> {
118 let text = std::str::from_utf8(bytes).map_err(|_| "bynk.embed.invalid_encoding")?;
119 if text.contains('\0') {
120 return Err("bynk.embed.binary_content");
121 }
122 let text = text.strip_prefix('\u{feff}').unwrap_or(text);
123 Ok(text.replace("\r\n", "\n").replace('\r', "\n"))
124}
125
126pub fn phase_embed_files(
134 parsed: &[ParsedFile],
135 available: Option<&BTreeSet<String>>,
136 errors: &mut ErrorSink,
137) {
138 let Some(available) = available else { return };
139 for pf in parsed {
140 for item in pf.items() {
141 let CommonsItem::Embed(e) = item else {
142 continue;
143 };
144 if path_errors(e).is_some() {
145 continue;
146 }
147 let key = bynk_project::assets::normalise_asset_path(&e.path);
148 if !available.contains(&key) {
149 errors.push_for(
150 Some(&pf.identity_path()),
151 CompileError::new(
152 "bynk.embed.missing_file",
153 e.path_span,
154 format!("embed `{}` names `{key}`, which does not exist", e.name.name),
155 )
156 .with_note(
157 "an embed must be a regular file in the project's `embeds/` folder; symbolic links are not followed",
158 ),
159 );
160 }
161 }
162 }
163}
164
165pub fn read_declared_embeds(
175 parsed: &[ParsedFile],
176 folder: &bynk_project::assets::EmbedFolder,
177 errors: &mut ErrorSink,
178) -> BTreeMap<String, String> {
179 let mut out: BTreeMap<String, String> = BTreeMap::new();
180 let mut refused: BTreeMap<String, (&'static str, String)> = BTreeMap::new();
183 let mut total = 0usize;
184 for pf in parsed {
185 for item in pf.items() {
186 let CommonsItem::Embed(e) = item else {
187 continue;
188 };
189 if path_errors(e).is_some() {
190 continue;
191 }
192 let key = bynk_project::assets::normalise_asset_path(&e.path);
193 if !folder.files().contains(&key) || out.contains_key(&key) {
194 continue;
195 }
196 if !refused.contains_key(&key) {
197 match read_one(folder, &key, total) {
198 Ok((text, raw_len)) => {
199 total += raw_len;
200 out.insert(key.clone(), text);
201 continue;
202 }
203 Err(refusal) => {
204 refused.insert(key.clone(), refusal);
205 }
206 }
207 }
208 let (code, detail) = &refused[&key];
209 errors.push_for(
210 Some(&pf.identity_path()),
211 refusal_error(code, e, &key, detail),
212 );
213 }
214 }
215 out
216}
217
218fn read_one(
221 folder: &bynk_project::assets::EmbedFolder,
222 key: &str,
223 total: usize,
224) -> Result<(String, usize), (&'static str, String)> {
225 let bytes = folder.read(key).map_err(|e| {
226 (
227 "bynk.embed.unreadable_file",
228 format!("the read failed: {e}"),
229 )
230 })?;
231 if bytes.len() > MAX_EMBED_BYTES {
232 return Err((
233 "bynk.embed.too_large",
234 format!(
235 "it is {} bytes, over the {MAX_EMBED_BYTES}-byte limit for one embedded file",
236 bytes.len()
237 ),
238 ));
239 }
240 if total + bytes.len() > MAX_EMBEDS_TOTAL_BYTES {
241 return Err((
242 "bynk.embed.too_large",
243 format!(
244 "with it the project's embedded files would pass {MAX_EMBEDS_TOTAL_BYTES} bytes in all"
245 ),
246 ));
247 }
248 embedded_text(&bytes)
249 .map(|text| (text, bytes.len()))
250 .map_err(|code| {
251 let detail = if code == "bynk.embed.invalid_encoding" {
252 "it is not valid UTF-8"
253 } else {
254 "it contains a NUL byte, so it is a binary file"
255 };
256 (code, detail.to_string())
257 })
258}
259
260fn refusal_error(code: &'static str, e: &EmbedDecl, key: &str, detail: &str) -> CompileError {
261 let (what, note) = match code {
262 "bynk.embed.unreadable_file" => ("could not be read", None),
263 "bynk.embed.invalid_encoding" => (
264 "is not UTF-8 text",
265 Some(
266 "an embed is compiled into the code as a `String`; serve a binary file with `asset` instead",
267 ),
268 ),
269 "bynk.embed.binary_content" => (
270 "is not text",
271 Some(
272 "an embed is compiled into the code as a `String`; serve a binary file with `asset` instead",
273 ),
274 ),
275 _ => (
276 "is too large to embed",
277 Some(
278 "an embed is for markup and prose; the text lands in the Worker bundle, so keep large data out of it",
279 ),
280 ),
281 };
282 let mut err = CompileError::new(
283 code,
284 e.path_span,
285 format!("embed `{}` names `{key}`, which {what}", e.name.name),
286 )
287 .with_note(detail.to_string());
288 if let Some(note) = note {
289 err = err.with_note(note);
290 }
291 err
292}
293
294pub fn name_error(
302 e: &EmbedDecl,
303 prev_embed: Option<&Ident>,
304 prev_asset: Option<&Ident>,
305 prev_fn: Option<&Ident>,
306 prev_type: Option<&Ident>,
307) -> Option<CompileError> {
308 let name = &e.name.name;
309 if let Some(prev) = prev_embed {
310 return Some(
311 CompileError::new(
312 "bynk.embed.duplicate",
313 e.name.span,
314 format!("embed `{name}` is already declared"),
315 )
316 .with_label(prev.span, "previously declared here"),
317 );
318 }
319 let (kind, prev) = prev_asset
320 .map(|p| ("an asset", p))
321 .or_else(|| prev_fn.map(|p| ("a function", p)))
322 .or_else(|| prev_type.map(|p| ("a type", p)))?;
323 Some(
324 CompileError::new(
325 "bynk.resolve.name_conflict",
326 e.name.span,
327 format!("embed `{name}` conflicts with {kind} of the same name"),
328 )
329 .with_label(prev.span, "declared here"),
330 )
331}
332
333pub fn single_file_embed_errors(items: &[CommonsItem], build: bool) -> Vec<CompileError> {
339 let mut fns: HashMap<&str, &Ident> = HashMap::new();
340 let mut types: HashMap<&str, &Ident> = HashMap::new();
341 let mut assets: HashMap<&str, &Ident> = HashMap::new();
342 for item in items {
343 match item {
344 CommonsItem::Fn(f) => {
345 if let FnName::Free(id) = &f.name {
346 fns.entry(id.name.as_str()).or_insert(id);
347 }
348 }
349 CommonsItem::Type(t) => {
350 types.entry(t.name.name.as_str()).or_insert(&t.name);
351 }
352 CommonsItem::Event(ev) => {
353 types.entry(ev.name.name.as_str()).or_insert(&ev.name);
354 }
355 CommonsItem::Asset(a) => {
356 assets.entry(a.name.name.as_str()).or_insert(&a.name);
357 }
358 _ => {}
359 }
360 }
361 let mut embeds: HashMap<&str, &Ident> = HashMap::new();
362 let mut errors = Vec::new();
363 for item in items {
364 let CommonsItem::Embed(e) = item else {
365 continue;
366 };
367 errors.extend(path_errors(e));
368 let key = e.name.name.as_str();
369 match name_error(
370 e,
371 embeds.get(key).copied(),
372 assets.get(key).copied(),
373 fns.get(key).copied(),
374 types.get(key).copied(),
375 ) {
376 Some(err) => errors.push(err),
377 None => {
378 embeds.insert(key, &e.name);
379 }
380 }
381 if build {
382 errors.push(needs_project_error(e));
383 }
384 }
385 errors
386}
387
388pub fn compose_unit_embeds(
395 name: &str,
396 unit_info: &BTreeMap<String, UnitInfo>,
397) -> HashMap<String, EmbedDecl> {
398 let Some(info) = unit_info.get(name) else {
399 return HashMap::new();
400 };
401 let mut combined = info.table.embeds.clone();
402 for used in &info.uses {
403 let Some(used_info) = unit_info.get(used) else {
404 continue;
405 };
406 for (embed_name, decl) in &used_info.table.embeds {
407 if crate::symbols::local_value_kind(&info.table, embed_name).is_some() {
408 continue;
409 }
410 combined
411 .entry(embed_name.clone())
412 .or_insert_with(|| decl.clone());
413 }
414 }
415 combined
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 #[test]
423 fn text_is_utf8_without_a_bom_or_carriage_returns() {
424 assert_eq!(embedded_text(b"a\nb\n").unwrap(), "a\nb\n");
425 assert_eq!(embedded_text(b"\xef\xbb\xbfhi").unwrap(), "hi");
427 assert_eq!(
428 embedded_text("a\u{feff}b".as_bytes()).unwrap(),
429 "a\u{feff}b"
430 );
431 assert_eq!(embedded_text(b"a\r\nb\rc\r\n").unwrap(), "a\nb\nc\n");
433 assert_eq!(embedded_text(b"").unwrap(), "");
435 assert_eq!(embedded_text("é\u{2028}".as_bytes()).unwrap(), "é\u{2028}");
436 }
437
438 #[test]
439 fn non_text_is_refused_with_its_code() {
440 assert_eq!(
441 embedded_text(b"\xff\xfe"),
442 Err("bynk.embed.invalid_encoding")
443 );
444 assert_eq!(
446 embedded_text(b"\xe2\x82"),
447 Err("bynk.embed.invalid_encoding")
448 );
449 assert_eq!(embedded_text(b"a\0b"), Err("bynk.embed.binary_content"));
450 assert_eq!(embedded_text(b"\xff\0"), Err("bynk.embed.invalid_encoding"));
452 }
453}