@@ -32,7 +32,14 @@ const FALLBACK_PATH_ENV: &str = "FALLBACK_PATH";
3232const CUSTOM_404_PATH_ENV : & str = "CUSTOM_404_PATH" ;
3333/// Directory fallback path (trying to map `/about/` -> `/about/index.html`).
3434const DIRECTORY_FALLBACK_PATH : & str = "index.html" ;
35-
35+ // FAVICON_ICO_FILENAME
36+ const FAVICON_ICO_FILENAME : & str = "favicon.ico" ;
37+ // FAVICON_PNG_FILENAME
38+ const FAVICON_PNG_FILENAME : & str = "favicon.png" ;
39+ // Fallback favicon.png that is used when user does not supply a custom one
40+ const FALLBACK_FAVICON_PNG : & [ u8 ] = include_bytes ! ( "../spin-favicon.png" ) ;
41+ // Fallback favicon.ico that is used when user does not supply a custom one
42+ const FALLBACK_FAVICON_ICO : & [ u8 ] = include_bytes ! ( "../spin-favicon.ico" ) ;
3643/// Common Content Encodings
3744#[ derive( Debug , Eq , PartialEq ) ]
3845pub enum ContentEncoding {
@@ -82,19 +89,55 @@ fn serve(req: Request) -> Result<Response> {
8289 // resolve the requested path and then try to read the file
8390 // None should indicate that the file does not exist after attempting fallback paths
8491 let body = match FileServer :: resolve ( path) {
85- Some ( path) => FileServer :: read ( & path, & enc) . ok ( ) ,
86- None => None ,
92+ FileServerPath :: Physical ( path) => FileServer :: read ( & path, & enc) . ok ( ) ,
93+ FileServerPath :: Embedded ( resource) => ResourceServer :: read ( resource, & enc) ,
94+ FileServerPath :: None => None ,
8795 } ;
88-
8996 let etag = FileServer :: get_etag ( body. clone ( ) ) ;
9097 FileServer :: send ( body, path, enc, & etag, if_none_match)
9198}
9299
100+ struct ResourceServer { }
101+
102+ impl ResourceServer {
103+ fn read ( resource : & ' static [ u8 ] , encoding : & ContentEncoding ) -> Option < Bytes > {
104+ match encoding {
105+ ContentEncoding :: Brotli => {
106+ let mut r = brotli:: CompressorReader :: new ( resource, 4096 , BROTLI_LEVEL , 20 ) ;
107+ let mut buf = vec ! [ ] ;
108+ r. read_to_end ( & mut buf) . ok ( ) ?;
109+ Some ( buf. into ( ) )
110+ }
111+ _ => Some ( resource. into ( ) ) ,
112+ }
113+ }
114+ }
115+
116+ #[ derive( Debug , Eq , PartialEq ) ]
117+ enum FileServerPath {
118+ Physical ( PathBuf ) ,
119+ Embedded ( & ' static [ u8 ] ) ,
120+ None ,
121+ }
122+
123+ trait IsFavicon {
124+ fn is_favicon ( & self ) -> bool ;
125+ }
126+
127+ impl IsFavicon for PathBuf {
128+ fn is_favicon ( & self ) -> bool {
129+ match self . clone ( ) . file_name ( ) {
130+ Some ( s) => s == FAVICON_ICO_FILENAME || s == FAVICON_PNG_FILENAME ,
131+ None => false ,
132+ }
133+ }
134+ }
135+
93136struct FileServer ;
94137impl FileServer {
95138 /// Resolve the request path to a file path.
96- /// Returns `None` if the path does not exist .
97- fn resolve ( req_path : & str ) -> Option < PathBuf > {
139+ /// Returns a `FileServerPath` variant .
140+ fn resolve ( req_path : & str ) -> FileServerPath {
98141 // fallback to index.html if the path is empty
99142 let mut path = if req_path. is_empty ( ) {
100143 PathBuf :: from ( DIRECTORY_FALLBACK_PATH )
@@ -107,6 +150,17 @@ impl FileServer {
107150 path. push ( DIRECTORY_FALLBACK_PATH ) ;
108151 }
109152
153+ // if path doesn't exist and a favicon is requested, return with corresponding embedded resource
154+ if !path. exists ( ) && path. is_favicon ( ) {
155+ return match path. extension ( ) {
156+ Some ( os_string) => match os_string. to_str ( ) {
157+ Some ( "ico" ) => FileServerPath :: Embedded ( FALLBACK_FAVICON_ICO ) ,
158+ Some ( "png" ) => FileServerPath :: Embedded ( FALLBACK_FAVICON_PNG ) ,
159+ _ => FileServerPath :: None ,
160+ } ,
161+ None => FileServerPath :: None ,
162+ } ;
163+ }
110164 // if still haven't found a file, override with the user-configured fallback path
111165 if !path. exists ( ) {
112166 if let Ok ( fallback_path) = std:: env:: var ( FALLBACK_PATH_ENV ) {
@@ -115,7 +169,7 @@ impl FileServer {
115169 }
116170
117171 if path. exists ( ) {
118- return Some ( path) ;
172+ return FileServerPath :: Physical ( path) ;
119173 }
120174
121175 // check if user configured a custom 404 path
@@ -125,9 +179,9 @@ impl FileServer {
125179 }
126180
127181 if path. exists ( ) {
128- Some ( path)
182+ FileServerPath :: Physical ( path)
129183 } else {
130- None
184+ FileServerPath :: None
131185 }
132186 }
133187
@@ -149,8 +203,13 @@ impl FileServer {
149203
150204 /// Return the media type of the file based on the path.
151205 fn mime ( uri : & str ) -> Option < String > {
152- let guess = mime_guess:: from_path ( uri) ;
153- guess. first ( ) . map ( |m| m. to_string ( ) )
206+ match uri {
207+ FAVICON_ICO_FILENAME => mime_guess:: from_ext ( "ico" ) ,
208+ FAVICON_PNG_FILENAME => mime_guess:: from_ext ( "png" ) ,
209+ _ => mime_guess:: from_path ( uri) ,
210+ }
211+ . first ( )
212+ . map ( |m| m. to_string ( ) )
154213 }
155214
156215 fn append_headers (
@@ -404,4 +463,22 @@ mod tests {
404463 let rsp = <super :: SpinHttp as spin_http:: SpinHttp >:: handle_http_request ( req) ;
405464 assert_eq ! ( rsp. status, 200 ) ;
406465 }
466+
467+ #[ test]
468+ fn test_serve_fallback_favicon ( ) {
469+ let req = spin_http:: Request {
470+ method : spin_http:: Method :: Get ,
471+ uri : "http://thisistest.com/" . to_string ( ) ,
472+ headers : vec ! [ (
473+ PATH_INFO_HEADER . to_string( ) ,
474+ FAVICON_PNG_FILENAME . to_string( ) ,
475+ ) ] ,
476+ params : vec ! [ ] ,
477+ body : None ,
478+ } ;
479+ let rsp = <super :: SpinHttp as spin_http:: SpinHttp >:: handle_http_request ( req) ;
480+
481+ assert_eq ! ( rsp. status, StatusCode :: OK ) ;
482+ assert_eq ! ( rsp. body. unwrap( ) , FALLBACK_FAVICON_PNG ) ;
483+ }
407484}
0 commit comments