macro_helpers.rs 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. pub use const_serialize;
  2. use const_serialize::{serialize_const, ConstStr, ConstVec};
  3. use manganis_core::{AssetOptions, BundledAsset};
  4. use crate::hash::ConstHasher;
  5. /// Create a bundled asset from the input path, the content hash, and the asset options
  6. pub const fn create_bundled_asset(
  7. input_path: &str,
  8. content_hash: &[u8],
  9. asset_config: AssetOptions,
  10. ) -> BundledAsset {
  11. let hashed_path = generate_unique_path_with_byte_hash(input_path, content_hash, &asset_config);
  12. BundledAsset::new_from_const(ConstStr::new(input_path), hashed_path, asset_config)
  13. }
  14. /// Create a bundled asset from the input path, the content hash, and the asset options with a relative asset deprecation warning
  15. ///
  16. /// This method is deprecated and will be removed in a future release.
  17. #[deprecated(
  18. note = "Relative asset!() paths are not supported. Use a path like `/assets/myfile.png` instead of `./assets/myfile.png`"
  19. )]
  20. pub const fn create_bundled_asset_relative(
  21. input_path: &str,
  22. content_hash: &[u8],
  23. asset_config: AssetOptions,
  24. ) -> BundledAsset {
  25. create_bundled_asset(input_path, content_hash, asset_config)
  26. }
  27. /// Format the input path with a hash to create an unique output path for the macro in the form `{input_path}-{hash}.{extension}`
  28. pub const fn generate_unique_path(
  29. input_path: &str,
  30. content_hash: u64,
  31. asset_config: &AssetOptions,
  32. ) -> ConstStr {
  33. let byte_hash = content_hash.to_le_bytes();
  34. generate_unique_path_with_byte_hash(input_path, &byte_hash, asset_config)
  35. }
  36. /// Format the input path with a hash to create an unique output path for the macro in the form `{input_path}-{hash}.{extension}`
  37. const fn generate_unique_path_with_byte_hash(
  38. input_path: &str,
  39. content_hash: &[u8],
  40. asset_config: &AssetOptions,
  41. ) -> ConstStr {
  42. // Format the unique path with the format `{input_path}-{hash}.{extension}`
  43. // Start with the input path
  44. let mut input_path = ConstStr::new(input_path);
  45. // Then strip the prefix from the input path. The path comes from the build platform, but
  46. // in wasm, we don't know what the path separator is from the build platform. We need to
  47. // split by both unix and windows paths and take the smallest one
  48. let mut extension = None;
  49. match (input_path.rsplit_once('/'), input_path.rsplit_once('\\')) {
  50. (Some((_, unix_new_input_path)), Some((_, windows_new_input_path))) => {
  51. input_path = if unix_new_input_path.len() < windows_new_input_path.len() {
  52. unix_new_input_path
  53. } else {
  54. windows_new_input_path
  55. };
  56. }
  57. (Some((_, unix_new_input_path)), _) => {
  58. input_path = unix_new_input_path;
  59. }
  60. (_, Some((_, windows_new_input_path))) => {
  61. input_path = windows_new_input_path;
  62. }
  63. _ => {}
  64. }
  65. if let Some((new_input_path, new_extension)) = input_path.rsplit_once('.') {
  66. extension = Some(new_extension);
  67. input_path = new_input_path;
  68. }
  69. // Then add a dash
  70. let mut macro_output_path = input_path.push_str("-");
  71. // Hash the contents along with the asset config to create a unique hash for the asset
  72. // When this hash changes, the client needs to re-fetch the asset
  73. let mut hasher = ConstHasher::new();
  74. hasher = hasher.write(content_hash);
  75. hasher = hasher.hash_by_bytes(asset_config);
  76. let hash = hasher.finish();
  77. // Then add the hash in hex form
  78. let hash_bytes = hash.to_le_bytes();
  79. let mut i = 0;
  80. while i < hash_bytes.len() {
  81. let byte = hash_bytes[i];
  82. let first = byte >> 4;
  83. let second = byte & 0x0f;
  84. const fn byte_to_char(byte: u8) -> char {
  85. match char::from_digit(byte as u32, 16) {
  86. Some(c) => c,
  87. None => panic!("byte must be a valid digit"),
  88. }
  89. }
  90. macro_output_path = macro_output_path.push(byte_to_char(first));
  91. macro_output_path = macro_output_path.push(byte_to_char(second));
  92. i += 1;
  93. }
  94. // Finally add the extension
  95. match asset_config.extension() {
  96. Some(extension) => {
  97. macro_output_path = macro_output_path.push('.');
  98. macro_output_path = macro_output_path.push_str(extension)
  99. }
  100. None => {
  101. if let Some(extension) = extension {
  102. macro_output_path = macro_output_path.push('.');
  103. let ext_bytes = extension.as_str().as_bytes();
  104. // Rewrite scss as css
  105. if bytes_equal(ext_bytes, b"scss") || bytes_equal(ext_bytes, b"sass") {
  106. macro_output_path = macro_output_path.push_str("css")
  107. } else {
  108. macro_output_path = macro_output_path.push_str(extension.as_str())
  109. }
  110. }
  111. }
  112. }
  113. macro_output_path
  114. }
  115. /// Construct the hash used by manganis and cli-opt to uniquely identify a asset based on its contents
  116. pub const fn hash_asset(asset_config: &AssetOptions, content_hash: u64) -> ConstStr {
  117. let mut string = ConstStr::new("");
  118. // Hash the contents along with the asset config to create a unique hash for the asset
  119. // When this hash changes, the client needs to re-fetch the asset
  120. let mut hasher = ConstHasher::new();
  121. hasher = hasher.write(&content_hash.to_le_bytes());
  122. hasher = hasher.hash_by_bytes(asset_config);
  123. let hash = hasher.finish();
  124. // Then add the hash in hex form
  125. let hash_bytes = hash.to_le_bytes();
  126. let mut i = 0;
  127. while i < hash_bytes.len() {
  128. let byte = hash_bytes[i];
  129. let first = byte >> 4;
  130. let second = byte & 0x0f;
  131. const fn byte_to_char(byte: u8) -> char {
  132. match char::from_digit(byte as u32, 16) {
  133. Some(c) => c,
  134. None => panic!("byte must be a valid digit"),
  135. }
  136. }
  137. string = string.push(byte_to_char(first));
  138. string = string.push(byte_to_char(second));
  139. i += 1;
  140. }
  141. string
  142. }
  143. const fn bytes_equal(left: &[u8], right: &[u8]) -> bool {
  144. if left.len() != right.len() {
  145. return false;
  146. }
  147. let mut i = 0;
  148. while i < left.len() {
  149. if left[i] != right[i] {
  150. return false;
  151. }
  152. i += 1;
  153. }
  154. true
  155. }
  156. #[test]
  157. fn test_unique_path() {
  158. use manganis_core::{ImageAssetOptions, ImageFormat};
  159. use std::path::PathBuf;
  160. let mut input_path = PathBuf::from("some");
  161. input_path.push("prefix");
  162. input_path.push("test.png");
  163. let content_hash = 123456789;
  164. let asset_config = AssetOptions::Image(ImageAssetOptions::new().with_format(ImageFormat::Avif));
  165. let output_path =
  166. generate_unique_path(&input_path.to_string_lossy(), content_hash, &asset_config);
  167. assert_eq!(output_path.as_str(), "test-603a88fe296462a3.avif");
  168. // Changing the path without changing the contents shouldn't change the hash
  169. let mut input_path = PathBuf::from("some");
  170. input_path.push("prefix");
  171. input_path.push("prefix");
  172. input_path.push("test.png");
  173. let content_hash = 123456789;
  174. let asset_config = AssetOptions::Image(ImageAssetOptions::new().with_format(ImageFormat::Avif));
  175. let output_path =
  176. generate_unique_path(&input_path.to_string_lossy(), content_hash, &asset_config);
  177. assert_eq!(output_path.as_str(), "test-603a88fe296462a3.avif");
  178. let mut input_path = PathBuf::from("test");
  179. input_path.push("ing");
  180. input_path.push("test");
  181. let content_hash = 123456789;
  182. let asset_config = AssetOptions::Unknown;
  183. let output_path =
  184. generate_unique_path(&input_path.to_string_lossy(), content_hash, &asset_config);
  185. assert_eq!(output_path.as_str(), "test-8d6e32dc0b45f853");
  186. // Just changing the content hash should change the total hash
  187. let mut input_path = PathBuf::from("test");
  188. input_path.push("ing");
  189. input_path.push("test");
  190. let content_hash = 123456780;
  191. let asset_config = AssetOptions::Unknown;
  192. let output_path =
  193. generate_unique_path(&input_path.to_string_lossy(), content_hash, &asset_config);
  194. assert_eq!(output_path.as_str(), "test-40783366737abc4d");
  195. }
  196. /// Serialize an asset to a const buffer
  197. pub const fn serialize_asset(asset: &BundledAsset) -> ConstVec<u8> {
  198. let write = ConstVec::new();
  199. serialize_const(asset, write)
  200. }
  201. /// Copy a slice into a constant sized buffer at compile time
  202. pub const fn copy_bytes<const N: usize>(bytes: &[u8]) -> [u8; N] {
  203. let mut out = [0; N];
  204. let mut i = 0;
  205. while i < N {
  206. out[i] = bytes[i];
  207. i += 1;
  208. }
  209. out
  210. }