Carry the terms a 51Did was created under in the identifier - #132
Merged
Conversation
A 51Did created for marketing may only be used by a receiver that has accepted the terms it was created under, and until now the identifier did not say which terms those are, so the answer had to travel beside it. In OpenRTB there is somewhere to put it, being the Terms Document Locator on the eids entry, and everywhere else there is not. An identifier passed as a query string parameter arrives on its own and any hop can drop a sidecar without the identifier looking any different, so a receiver that gets one without its terms cannot act on it safely. One byte after the match key now answers that, and it is an index into a table in the specification rather than a version number, so a later document can live at any address rather than only at one a number could compose. Terms names the values, being NOT_STATED for index 0, MODEL_TERMS_FOR_MARKETING_2 for index 1 and UNKNOWN for an index this package does not know. FodId.getTerms() answers the named value, getTermsIndex() the raw byte and getTermsUrl() the address, which is null for both NOT_STATED and UNKNOWN and is never an empty string and never built from the index. The package never fetches the address, it returns it. UNKNOWN is deliberately not NOT_STATED. Zero says no terms are stated whilst an unknown index says terms are stated that this package cannot name, and a receiver confusing the two would read an identifier created under terms as one created under none. The raw index is exposed alongside the named value so that a caller meeting an index added after this release can look the document up by hand and can report which index it could not read. Existing identifiers are unaffected. One issued before the byte existed has a payload that ends at the match key, and a missing byte reads as index 0, which says the terms are not stated in the identifier, so absence and zero mean the same thing, no reader has to tell them apart and no presence flag is needed. The byte adds nothing to the minimum payload lengths this package enforces, so no identifier that read before fails to read now, and every existing test passes untouched. Depends on 51Degrees/specifications#27, which must merge first. pipeline.did tests: 137 run, 0 failures, 2 skipped (the live cloud tests), five new. The fodid developer example prints the three members and its tests run 9, 0 failures.
The example module is outside pipeline.did and printing the new members there is a separate change, so it is taken back out and the diff is the package alone.
No reader has ever seen a 51Did without the Terms byte, so describing the field as a change from a previous state gives a reader history they cannot use. Every rule the history was wrapped around is kept and reworded to describe the payload instead. A payload that ends at the match key has no Terms byte, and a missing byte reads as index zero, which says the terms are not stated in the identifier, so absence and zero mean the same thing and no presence flag is needed. The Terms still adds nothing to the minimum payload lengths the package enforces.
…he Terms with its address Bits 4 and 5 of the Flags byte are the payload version. This package reads version 0 and refuses any other with FodIdParseStatus.UNSUPPORTED_PAYLOAD_VERSION, with the throwing readers naming the version they found. No field is read under the layout this package knows once the version says otherwise, because a later version exists precisely because a field moved, so reading such a payload here would answer with values that are wrong rather than absent. The version is not exposed, because either the package read the layout or there is no identifier to read fields from. FodIdParseResult carries the version it found so the message can name it, and that field is not public. The Terms is one member rather than three. getTerms() answers with the address of the document the identifier was created under, and the package turns the index into the address so a caller never handles the byte. The raw index and the separate address member are gone and the Terms enumeration is package-private. An index of zero and an index this package cannot name both answer with no address, which a caller cannot tell apart, and that is deliberate because both say the identifier does not give the terms and the answer has to come from somewhere else. No address is ever built from an index, since that would name a document nobody wrote. The test factory is the creating side, so it writes both new fields. The canonical Flags byte carries version 0, the canonical payload carries the Terms of a personalized marketing identifier and the canonical Random payload carries the zero a non-marketing identifier carries. A payload that ends at the match key is now a fixture of its own, since a reader takes it as a Terms of zero and no issuer would write one. pipeline.did tests: 142 run, 0 failures, 0 errors, 2 skipped, being the live cloud tests. Javadoc builds with no warnings.
The Reserved type takes every byte after the header as the match key, so no byte is left to read as the Terms and getTerms() answers null. The other five packages each test this and Java did not.
Adding a terms document meant editing the enumeration and a second switch that repeated the index, so the two could disagree. Each member now carries its index alongside its address, and the index to member map is built from the members in a static block rather than written out again, so fromIndex is one lookup and a new document is one new member and nothing else. A test walks every member and fails if one does not read back from its own index, or names a document with no address, which is what would happen if a member and its address were added apart. mvn test -pl pipeline.did: 34 in FodIdParseTests, 40 in FodIdTests, 0 failures.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft. The specification this implements has merged as 51Degrees/specifications#27, and this stays in draft until the cloud has been updated to write both fields. pipeline-dotnet#409 goes first, because the cloud is .NET and needs that package published before it can write anything.
What this is
Two fields of the 51Did payload, read by
pipeline.did.The Terms is one byte after the match key, saying which terms document the identifier was created under. A 51Did created for marketing may only be used by a receiver that has accepted the terms it was created under, so the identifier has to say which terms those are. In OpenRTB there is somewhere to put the answer, being the Terms Document Locator on the
eidsentry, and everywhere else there is not. An identifier passed as a query string parameter arrives on its own and any hop can drop an answer carried beside it without the identifier looking any different, so the terms travel inside the identifier.The payload version is bits 4 and 5 of the Flags byte, saying which layout the payload follows.
The Terms
The byte is an index into a table in the specification and is not a version number, so that a later document can live at any address rather than only at one a number could compose. An index is never reused and never repointed once published, because repointing one would rewrite what an identifier already issued says it agreed to.
getTerms()answers with the address of the document. The package turns the index into the address, so a caller never handles the byte.getTerms()0null1https://m4ow.uk/mtm/2.txtnullThat one member is the whole of the addition to the public surface. The package answers with the address and never fetches it, and the receiver decides what to do with the document.
The payload version
This package reads version 0 and refuses every other version with
FodIdParseStatus.UNSUPPORTED_PAYLOAD_VERSION, with the throwing readers naming the version they found in the message.FodIdParseResultcarries the version so the message can name it, and that field is package-private, since a caller has nothing to decide with it.No field is read under the layout this package knows once the version says otherwise. A later version exists precisely because a field moved, so reading such a payload here would answer with values that are wrong rather than absent, which is worse than refusing. A version that nothing checks protects nothing.
The version is not exposed. Either this package read the layout, in which case the accessors are the answer, or it did not, in which case there is no identifier to read fields from.
The parts that are easy to get wrong
No address is ever built from an index this package cannot name. That would name a document nobody wrote, and a receiver would record having accepted terms that do not exist. An index of zero and an index added after this release therefore give a caller the same answer, which is deliberate, since both say the identifier does not give the terms and the answer has to come from somewhere else.
The Terms byte is read at the offset the match key ends at. Bits 6-7 of the Flags byte select the type and the type sets the match key length, so the offset follows the type, and a reader with one fixed offset reads a match key byte on a
RANDOMidentifier.A payload with no byte after the match key reads as index 0. That is the same answer a byte holding zero gives, so absence and zero mean the same thing, no reader has to tell them apart and no presence flag or format version is needed. The byte is not counted in the minimum payload length for any identifier type.
A
RESERVEDidentifier reads as index 0. That type is not yet assigned and this package reads it best effort, taking every byte after the header as the match key, so no byte follows the match key. The specification asks for exactly that, since it says a payload with no byte after the match key is a Terms of zero, and reading the last context byte as a terms index instead would be worse. It is still worth stating, because aRESERVEDidentifier that did carry a Terms would be reported as carrying none, and nothing can be done about that until the type is assigned and the length of its match key is known.No address does not mean the identifier is unrestricted. It says only that the identifier does not carry the answer, so the answer has to come from the surrounding protocol. Where an identifier may go is a separate question
getUsage()answers, which still bars a non-marketing identifier from a demand source.The Terms sits before the creator context section. An issuer writing a context section writes the Terms byte before it. The lengths of that section belong to the cloud and this package holds no upper bound of its own, so a longer payload is accepted and the same four fields are read.
The version is read on its own bits. A reader masking the wrong bits would refuse a version 0 identifier of some usages, or let a later version through, so there is a test over every combination of the usage and type bits.
Files changed
pipeline.did/src/main/java/fiftyone/pipeline/did/Terms.java, the named values the specification gives for each index andfromIndex(int). It is package-private, since the package turns the index into an address and a caller never handles the byte.pipeline.did/src/main/java/fiftyone/pipeline/did/FodId.java, reads the version before any field, refuses a version it does not know, reads the Terms byte wherever the type puts the end of the match key, and addsgetTerms()aftergetMatchKey()so it sits in payload order.pipeline.did/src/main/java/fiftyone/pipeline/did/FodIdParseStatus.java,UNSUPPORTED_PAYLOAD_VERSION.pipeline.did/src/main/java/fiftyone/pipeline/did/FodIdParseResult.java, carries the refused version for the message.pipeline.did/src/main/java/fiftyone/pipeline/did/package-info.java, the payload fields a 51Did exposes.pipeline.did/src/test/java/fiftyone/pipeline/did/FodIdTestFactory.java, the creating side, so it writes both new fields.pipeline.did/src/test/java/fiftyone/pipeline/did/FodIdParseTests.java, the tests below.pipeline.did/README.md, the payload layout, a payload version section and a section on which terms a 51Did was created under.The change stays inside
pipeline.did. The developer example module is outside the package and printing the member there is a separate change.The test fixtures write what an issuer writes
canonicalPayload()andcanonicalRandomPayload()are the creating side, so they now carry the payload version 0 in the Flags byte and the Terms byte their usage calls for, being index 1 for the personalized marketing canonical payload and index 0 for the non-marketing Random one.payloadEndingAtMatchKey()andrandomPayloadEndingAtMatchKey()are the fixtures for a payload that carries no Terms byte, which a reader takes as index 0 and which no issuer would write.Tests
In
FodIdParseTests, over the Terms:getTerms_NoByteAfterTheMatchKey_HasNoAddress, a payload ending at the match key answers with no address, for both the 32 byte and the 16 byte type.getTerms_ZeroByte_AnswersAsAbsenceDoes, a byte holding zero answers exactly as no byte at all does.getTerms_IndexOne_ModelTermsAddressForBothKeyLengths, index 1 returnshttps://m4ow.uk/mtm/2.txtexactly, for both match key lengths, and each match key is compared against the same payload without the byte so neither loses a byte to the Terms.getTerms_IndexThisPackageDoesNotKnow_HasNoAddress, indexes 2, 127, 200 and 255 answer with no address. 255 is there because the byte is signed in Java and an unmasked read would answer -1.terms_EveryIndexOutsideTheTable_IsUnknownWithNoUrl, every one of the 254 indexes the table does not carry has no address.terms_EveryMemberAgreesWithTheTable, every member reads back from its own index and one that names a document has an https address.getTerms_ByteThenContextSection_ReadAtTheRightOffset, a payload carrying a Terms byte and then a 512 byte creator context section still reads the match key, the Terms and the whole payload, and still verifies.And over the payload version:
version_Zero_ReadsEveryField, version 0 reads the type, usage, match key and terms.version_NotZero_IsRefused, versions 1, 2 and 3 answerUNSUPPORTED_PAYLOAD_VERSIONand hand back nothing.version_NotZero_MessageNamesTheVersion, the message names the version it found.version_IsReadApartFromTheUsageAndTypeBits, every combination of the usage and type bits reads at version 0 and is refused at 1, 2 and 3.The terms table
The address, the index and the name are one table, so adding a terms document is one row and not a search for every place a number was written down. That is the point of the byte being an index rather than a version number, and the first version of this change did not honour it, spreading the same fact over two, being the enumeration and a switch that repeated the index places that could disagree.
Each member now carries its index alongside its address, and the index to member map is built from the members in a static block rather than written out again, so
fromIndexis one lookup and a new document is one new member.A test walks every member and fails if one does not read back from its own index, or names a document with no address, which is what would happen if a member and its address were added apart.
What was run and what it reported
Apache Maven 3.9.16 on OpenJDK 21, from the repository root.
FodIdParseTests32 run, 0 failures.FodIdTests40 run, 0 failures.DidClientTests68 run, 0 failures.DidClientLiveTests2 run, 2 skipped, being the tests that need the cloud. Module total 142 run, 0 failures, 0 errors, 2 skipped.BUILD SUCCESS, no warnings, so every link added resolves.
The value names
The specification fixes the names every package uses for the three values, which in the Java form are
NOT_STATED,MODEL_TERMS_FOR_MARKETING_2andUNKNOWN, so the constants here follow the table in 51Degrees/specifications#27 rather than a choice made in this pull request. The enum is package-private and the names are for a reader of the code, since the public surface is the address alone.