1/** 2 * @fileoverview Contains classes that hold data for a protobuf field. 3 */ 4 5goog.module('protobuf.binary.field'); 6 7const WireType = goog.requireType('protobuf.binary.WireType'); 8const Writer = goog.requireType('protobuf.binary.Writer'); 9const {checkDefAndNotNull, checkState} = goog.require('protobuf.internal.checks'); 10 11/** 12 * Number of bits taken to represent a wire type. 13 * @const {number} 14 */ 15const WIRE_TYPE_LENGTH_BITS = 3; 16 17/** @const {number} */ 18const WIRE_TYPE_EXTRACTOR = (1 << WIRE_TYPE_LENGTH_BITS) - 1; 19 20/** 21 * An IndexEntry consists of the wire type and the position of a field in the 22 * binary data. The wire type and the position are encoded into a single number 23 * to save memory, which can be decoded using Field.getWireType() and 24 * Field.getStartIndex() methods. 25 * @typedef {number} 26 */ 27let IndexEntry; 28 29/** 30 * An entry containing the index into the binary data and/or the corresponding 31 * cached JS object(s) for a field. 32 * @template T 33 * @final 34 * @package 35 */ 36class Field { 37 /** 38 * Creates a field and inserts the wireType and position of the first 39 * occurrence of a field. 40 * @param {!WireType} wireType 41 * @param {number} startIndex 42 * @return {!Field} 43 */ 44 static fromFirstIndexEntry(wireType, startIndex) { 45 return new Field([Field.encodeIndexEntry(wireType, startIndex)]); 46 } 47 48 /** 49 * @param {T} decodedValue The cached JS object decoded from the binary data. 50 * @param {function(!Writer, number, T):void|undefined} encoder Write function 51 * to encode the cache into binary bytes. 52 * @return {!Field} 53 * @template T 54 */ 55 static fromDecodedValue(decodedValue, encoder) { 56 return new Field(null, decodedValue, encoder); 57 } 58 59 /** 60 * @param {!WireType} wireType 61 * @param {number} startIndex 62 * @return {!IndexEntry} 63 */ 64 static encodeIndexEntry(wireType, startIndex) { 65 return startIndex << WIRE_TYPE_LENGTH_BITS | wireType; 66 } 67 68 /** 69 * @param {!IndexEntry} indexEntry 70 * @return {!WireType} 71 */ 72 static getWireType(indexEntry) { 73 return /** @type {!WireType} */ (indexEntry & WIRE_TYPE_EXTRACTOR); 74 } 75 76 /** 77 * @param {!IndexEntry} indexEntry 78 * @return {number} 79 */ 80 static getStartIndex(indexEntry) { 81 return indexEntry >> WIRE_TYPE_LENGTH_BITS; 82 } 83 84 /** 85 * @param {?Array<!IndexEntry>} indexArray 86 * @param {T=} decodedValue 87 * @param {function(!Writer, number, T):void=} encoder 88 * @private 89 */ 90 constructor(indexArray, decodedValue = undefined, encoder = undefined) { 91 checkState( 92 !!indexArray || decodedValue !== undefined, 93 'At least one of indexArray and decodedValue must be set'); 94 95 /** @private {?Array<!IndexEntry>} */ 96 this.indexArray_ = indexArray; 97 /** @private {T|undefined} */ 98 this.decodedValue_ = decodedValue; 99 // TODO: Consider storing an enum to represent encoder 100 /** @private {function(!Writer, number, T)|undefined} */ 101 this.encoder_ = encoder; 102 } 103 104 /** 105 * Adds a new IndexEntry. 106 * @param {!WireType} wireType 107 * @param {number} startIndex 108 */ 109 addIndexEntry(wireType, startIndex) { 110 checkDefAndNotNull(this.indexArray_) 111 .push(Field.encodeIndexEntry(wireType, startIndex)); 112 } 113 114 /** 115 * Returns the array of IndexEntry. 116 * @return {?Array<!IndexEntry>} 117 */ 118 getIndexArray() { 119 return this.indexArray_; 120 } 121 122 /** 123 * Caches the decoded value and sets the write function to encode cache into 124 * binary bytes. 125 * @param {T} decodedValue 126 * @param {function(!Writer, number, T):void|undefined} encoder 127 */ 128 setCache(decodedValue, encoder) { 129 this.decodedValue_ = decodedValue; 130 this.encoder_ = encoder; 131 this.maybeRemoveIndexArray_(); 132 } 133 134 /** 135 * If the decoded value has been set. 136 * @return {boolean} 137 */ 138 hasDecodedValue() { 139 return this.decodedValue_ !== undefined; 140 } 141 142 /** 143 * Returns the cached decoded value. The value needs to be set when this 144 * method is called. 145 * @return {T} 146 */ 147 getDecodedValue() { 148 // Makes sure that the decoded value in the cache has already been set. This 149 // prevents callers from doing `if (field.getDecodedValue()) {...}` to check 150 // if a value exist in the cache, because the check might return false even 151 // if the cache has a valid value set (e.g. 0 or empty string). 152 checkState(this.decodedValue_ !== undefined); 153 return this.decodedValue_; 154 } 155 156 /** 157 * Returns the write function to encode cache into binary bytes. 158 * @return {function(!Writer, number, T)|undefined} 159 */ 160 getEncoder() { 161 return this.encoder_; 162 } 163 164 /** 165 * Returns a copy of the field, containing the original index entries and a 166 * shallow copy of the cache. 167 * @return {!Field} 168 */ 169 shallowCopy() { 170 // Repeated fields are arrays in the cache. 171 // We have to copy the array to make sure that modifications to a repeated 172 // field (e.g. add) are not seen on a cloned accessor. 173 const copiedCache = this.hasDecodedValue() ? 174 (Array.isArray(this.getDecodedValue()) ? [...this.getDecodedValue()] : 175 this.getDecodedValue()) : 176 undefined; 177 return new Field(this.getIndexArray(), copiedCache, this.getEncoder()); 178 } 179 180 /** 181 * @private 182 */ 183 maybeRemoveIndexArray_() { 184 checkState( 185 this.encoder_ === undefined || this.decodedValue_ !== undefined, 186 'Encoder exists but decoded value doesn\'t'); 187 if (this.encoder_ !== undefined) { 188 this.indexArray_ = null; 189 } 190 } 191} 192 193exports = { 194 IndexEntry, 195 Field, 196}; 197