Encoder uses the static table.#10
Conversation
There was a problem hiding this comment.
Thank your for the PR. This looks pretty good already, just a few comments to simplify the code a bit.
I guess you introduced additional function parameters in preparation for dynamic table lookups, but I’d like to keep things as simple as possible in this PR.
Please also rebase your PR on the current master branch. I removed the failing Fuzzit build (they're shutting down their service), and added some linters.
encoder.go
Outdated
| } | ||
|
|
||
| e.writeLiteralFieldWithoutNameReference(f) | ||
| nameFound, valueFound, idx := e.getHeaderFieldIndex(&f) |
There was a problem hiding this comment.
It seems this would be easier if you don't define a getHeaderFieldIndex function.
It should also be faster, as you can skip the map lookup for valueFound if nameFound is false.
There was a problem hiding this comment.
Yeah, initially I thought I'd call it in several places but I ended up just making it harder to read.
Also, while inlining it I also found a bug:
I'm writing an indexed field if nameFound and len(f.Value) == 0 but that would be incorrect for some input.
For example {":method", "" } cannot be encoded with an indexed field.
Will add more test coverage.
encoder.go
Outdated
| // Encodes a header field whose name is present in one of the | ||
| // tables. TBit is true if the idx refers to the static table. | ||
| func (e *Encoder) writeLiteralFieldWithNameReference( | ||
| f *HeaderField, idx int, NBit, TBit bool) { |
There was a problem hiding this comment.
You don’t need NBit and TBit here. They are always false and true, respectively.
There was a problem hiding this comment.
Ok I'll remove them for this PR.
The T bit is strictly related to the dynamic table but I'm not sure the N bit is. For example the N bit is present in the encoding of a Literal Field Line Without Name Reference even though this doesn't use the dynamic table at all.
The RFC says
The following bit, 'N', indicates whether an intermediary is
permitted to add this field line to the dynamic table on subsequent hops.When
the 'N' bit is set, the encoded field line MUST always be encoded
with a literal representation. In particular, when a peer sends a
field line that it received represented as a literal field line with
the 'N' bit set, it MUST use a literal representation to forward this
field line. This bit is intended for protecting field values that
are not to be put at risk by compressing them; see Section 7 for more
details.
So even if our implementation doesn't use the dynamic table we should offer the caller the possibility to set the N bit so that the value is not cached by a subsequent proxy down the line.
That being said I also think this can be done in a subsequent PR.
static_table.go
Outdated
| // There's a second level of mapping for the headers that have some predefined | ||
| // values in the static table. | ||
| var encoderMap = map[string]indexAndValues{ | ||
| ":authority": indexAndValues{0, map[string]int{}}, |
There was a problem hiding this comment.
Why not use nil for values that don’t have a value?
There was a problem hiding this comment.
I just forgot this was possible in Go :)
static_table.go
Outdated
| } | ||
|
|
||
| type indexAndValues struct { | ||
| idx int |
There was a problem hiding this comment.
This could be a uint8, right?
| // and what value should be used to encode it. | ||
| // There's a second level of mapping for the headers that have some predefined | ||
| // values in the static table. | ||
| var encoderMap = map[string]indexAndValues{ |
There was a problem hiding this comment.
Can you add a unit test that checks that encoderMap and staticTableEntries are consistent, e.g. by looping over each one of them and checking that the indexes exist and match in the other table?
|
Regarding integration tests, the first step would be to add some in integration_test.go. I’d pick some entries from the static table (at random), and encode and then decode them. You could then check that the encoded value is shorter than it would be if the value hadn’t used the static table, e.g. by running two encodings: first with the actual value, then with the same value, but one letter replaced by something else (such that the table lookup fails). |
jstordeur
left a comment
There was a problem hiding this comment.
I also haven' t used git in a while so I might have merged instead of rebase...
I'm also adding a commit to address the comment, no idea how this is going to play out with the comments.
encoder.go
Outdated
| } | ||
|
|
||
| e.writeLiteralFieldWithoutNameReference(f) | ||
| nameFound, valueFound, idx := e.getHeaderFieldIndex(&f) |
There was a problem hiding this comment.
Yeah, initially I thought I'd call it in several places but I ended up just making it harder to read.
Also, while inlining it I also found a bug:
I'm writing an indexed field if nameFound and len(f.Value) == 0 but that would be incorrect for some input.
For example {":method", "" } cannot be encoded with an indexed field.
Will add more test coverage.
encoder.go
Outdated
| // Encodes a header field whose name is present in one of the | ||
| // tables. TBit is true if the idx refers to the static table. | ||
| func (e *Encoder) writeLiteralFieldWithNameReference( | ||
| f *HeaderField, idx int, NBit, TBit bool) { |
There was a problem hiding this comment.
Ok I'll remove them for this PR.
The T bit is strictly related to the dynamic table but I'm not sure the N bit is. For example the N bit is present in the encoding of a Literal Field Line Without Name Reference even though this doesn't use the dynamic table at all.
The RFC says
The following bit, 'N', indicates whether an intermediary is
permitted to add this field line to the dynamic table on subsequent hops.When
the 'N' bit is set, the encoded field line MUST always be encoded
with a literal representation. In particular, when a peer sends a
field line that it received represented as a literal field line with
the 'N' bit set, it MUST use a literal representation to forward this
field line. This bit is intended for protecting field values that
are not to be put at risk by compressing them; see Section 7 for more
details.
So even if our implementation doesn't use the dynamic table we should offer the caller the possibility to set the N bit so that the value is not cached by a subsequent proxy down the line.
That being said I also think this can be done in a subsequent PR.
static_table.go
Outdated
| // There's a second level of mapping for the headers that have some predefined | ||
| // values in the static table. | ||
| var encoderMap = map[string]indexAndValues{ | ||
| ":authority": indexAndValues{0, map[string]int{}}, |
There was a problem hiding this comment.
I just forgot this was possible in Go :)
static_table.go
Outdated
| } | ||
|
|
||
| type indexAndValues struct { | ||
| idx int |
| // and what value should be used to encode it. | ||
| // There's a second level of mapping for the headers that have some predefined | ||
| // values in the static table. | ||
| var encoderMap = map[string]indexAndValues{ |
Codecov Report
@@ Coverage Diff @@
## master #10 +/- ##
==========================================
+ Coverage 91.13% 92.07% +0.94%
==========================================
Files 4 4
Lines 203 227 +24
==========================================
+ Hits 185 209 +24
Misses 9 9
Partials 9 9
Continue to review full report at Codecov.
|
|
Hello @jstordeur, thank you! This looks good now. Can you please rebase on top of the current master and squash all commits into one? Also, there's a linter error here that we need to fix before merging. On master I also activated the |
|
Hi @jstordeur, would be great if you could help push this PR to the finish line. I'm planning to cut a new release of quic-go very soon, and would like to include this change. |
7035845 to
977e16d
Compare
|
I think this should be OK now, my git is very rusty... |
Yes, that would be good. As far as I can see, you didn't remove / include any new dependencies, so go.mod and go.sum should be unchanged. |
|
done, I'm off for today. Hopefully this is good to merge now. |
marten-seemann
left a comment
There was a problem hiding this comment.
Looks good to me!
Thank you for your contribution, @jstordeur. This will give us significant compression savings on H3 requests and responses.
A first pass at this, I haven't written any go in a while so bear with me.
I assume I should also add some support for the integration tests, how does this part work?