Skip to content

Commit 5c12c12

Browse files
authored
Merge pull request #96 from boydm/boyd
Document Cache apis, also clean up Cache.Hash functions
2 parents e535902 + 2242ec7 commit 5c12c12

File tree

8 files changed

+437
-138
lines changed

8 files changed

+437
-138
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* Add Graph.modify/3 with a finder function
1414
* Rename Cache.request_notification/1 -> Cache.subscribe/1
1515
* Rename Cache.stop_notification/1 -> Cache.unsubscribe/1
16-
16+
* General cleanup of Scenic.Cache.Hash. Some functions removed. Some function signatures changed.
1717

1818
## 0.8.0
1919

lib/scenic/cache.ex

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ defmodule Scenic.Cache do
8585
These scheme is much stronger when the application code itself is also signed and
8686
verified, but that is an exercise for the packaging tools.
8787
88-
Example:
88+
Full Example:
8989
9090
defmodule MyApp.MyScene do
9191
use Scenic.Scene
@@ -94,16 +94,24 @@ defmodule Scenic.Cache do
9494
@asset_path :code.priv_dir(:my_app) |> Path.join("/static/images/asset.jpg")
9595
9696
# pre-compute the hash (compile time)
97-
@asset_hash Scenic.Cache.Hash.file!( @bird_path, :sha )
97+
@asset_hash Scenic.Cache.Hash.file!( @asset_path, :sha )
98+
99+
# build a graph that uses the asset (compile time)
100+
@graph Scenic.Graph.build()
101+
|> rect( {100, 100}, fill: {:image, @asset_hash} )
102+
98103
99104
def init( _, _ ) {
100105
# load the asset into the cache (run time)
101106
Scenic.Cache.File.load(@asset_path, @asset_hash)
102107
103-
...
104-
}
108+
# push the graph. (run time)
109+
push_graph(@graph)
105110
106-
...
111+
{:ok, @graph}
112+
end
113+
114+
end
107115
108116
When assets are loaded this way, the `@asset_hash` term is also used as the key in
109117
the cache. This has the additional benefit of allowing you to pre-compute

lib/scenic/cache/file.ex

Lines changed: 120 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,115 @@
55

66
defmodule Scenic.Cache.File do
77
@moduledoc """
8-
Simple functions to load a file, following the hashing rules
8+
Helpers for loading files directly into the cache.
9+
10+
Static assets such as fonts and images are usually stored as files on the local
11+
storage device. These files need to be loaded into the cache in order to be used
12+
by the various parts of Scenic.
13+
14+
## Where to store your static file assets
15+
16+
You can store your assets anywhere in your app's `priv/` directory. This directory is
17+
special in the sense that the Elixir build system knows to copy its contents into the
18+
correct final build location. How you organize your assets inside of `priv/` is up to you.
19+
20+
my_app/
21+
priv/
22+
static/
23+
images/
24+
asset.jpg
25+
26+
27+
At compile time you need to build the actual path of your asset by combining
28+
the build directory with the partial path inside of `priv/`
29+
30+
Example
31+
32+
path = :code.priv_dir(:my_app)
33+
|> Path.join("/static/images/asset.jpg")
34+
35+
You can do this at either compile time or runtime.
36+
37+
## Security
38+
39+
A lesson learned the hard way is that static assets (fonts, images, etc) that your app
40+
loads out of storage can easily become attack vectors.
41+
42+
These formats are complicated! There is no guarantee (on any system) that a malformed
43+
asset will not cause an error in the C code that interprets it. Again - these are complicated
44+
and the renderers need to be fast...
45+
46+
The solution is to compute a SHA hash of these files during build-time of your
47+
and to store the result in your applications code itself. Then during run time, you
48+
compare then pre-computed hash against the run-time of the asset being loaded.
49+
50+
These scheme is much stronger when the application code itself is also signed and
51+
verified, but that is an exercise for the packaging tools.
52+
53+
When assets are loaded this way, the `@asset_hash` term is also used as the key in
54+
the cache. This has the additional benefit of allowing you to pre-compute
55+
the graph itself, using the correct keys for the correct assets.
56+
57+
## Full example
58+
59+
defmodule MyApp.MyScene do
60+
use Scenic.Scene
61+
62+
# build the path to the static asset file (compile time)
63+
@asset_path :code.priv_dir(:my_app) |> Path.join("/static/images/asset.jpg")
64+
65+
# pre-compute the hash (compile time)
66+
@asset_hash Scenic.Cache.Hash.file!( @asset_path, :sha )
67+
68+
# build a graph that uses the asset (compile time)
69+
@graph Scenic.Graph.build()
70+
|> rect( {100, 100}, fill: {:image, @asset_hash} )
71+
72+
def init( _, _ ) {
73+
# load the asset into the cache (run time)
74+
Scenic.Cache.File.load(@asset_path, @asset_hash)
75+
76+
# push the graph. (run time)
77+
push_graph(@graph)
78+
79+
{:ok, @graph}
80+
end
81+
82+
end
983
"""
1084
alias Scenic.Cache
1185
alias Scenic.Cache.Hash
1286

13-
# import IEx
14-
1587
# --------------------------------------------------------
88+
@doc """
89+
Load a file directly into the cache.
90+
91+
Parameters:
92+
* `path` - the path to the asset file
93+
* `hash` - the pre-computed hash of the file
94+
* `opts` - a list of options. See below.
95+
96+
Options:
97+
* `hash` - format of the hash. Valid formats include `:sha, :sha224, :sha256, :sha384, :sha512, :ripemd160`. If the hash option is not set, it will use `:sha` by default.
98+
* `scope` - Explicitly set the scope of the asset in the cache.
99+
* `decompress` - if `true` - decompress the data (zlib) after reading and verifying the hash.
100+
101+
On success, returns
102+
`{:ok, cache_key}`
103+
104+
The key in the cache will be the hash of the file.
105+
"""
16106
def load(path, hash, opts \\ [])
17107

18108
# insecure loading. Loads file blindly even it is altered
19109
# don't recommend doing this in production. Better to embed the expected
20110
# hashes. Is also slower because it has to load the file and compute the hash
21111
# to use as a key even it is is already loaded into the cache.
22112
def load(path, :insecure, opts) do
113+
if Mix.env() != :test do
114+
IO.puts("WARNING: Cache asset loaded as :insecure \"#{path}\"")
115+
end
116+
23117
with {:ok, data} <- read(path, :insecure, opts) do
24118
hash = Hash.binary(data, opts[:hash] || :sha)
25119

@@ -53,13 +147,36 @@ defmodule Scenic.Cache.File do
53147
end
54148

55149
# --------------------------------------------------------
150+
@doc """
151+
Read a file into memory.
152+
153+
The reason you would use this instead of File.read is to verify the data against
154+
a pre-computed hash.
155+
156+
Parameters:
157+
* `path` - the path to the asset file
158+
* `hash` - the pre-computed hash of the file
159+
* `opts` - a list of options. See below.
160+
161+
Options:
162+
* `hash` - format of the hash. Valid formats include `:sha, :sha224, :sha256, :sha384, :sha512, :ripemd160`. If the hash option is not set, it will use `:sha` by default.
163+
* `decompress` - if `true` - decompress the data (zlib) after reading and verifying the hash.
164+
165+
On success, returns
166+
`{:ok, data}`
167+
"""
168+
56169
def read(path, hash, opts \\ [])
57170

58171
# insecure read
59172
# don't recommend doing this in production. Better to embed the expected
60173
# hashes. Is also slower because it has to load the file and compute the hash
61174
# to use as a key even it is is already loaded into the cache.
62175
def read(path, :insecure, opts) do
176+
if Mix.env() != :test do
177+
IO.puts("WARNING: Cache asset read as :insecure \"#{path}\"")
178+
end
179+
63180
with {:ok, data} <- File.read(path) do
64181
do_unzip(data, opts)
65182
else

0 commit comments

Comments
 (0)