| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| |
| from __future__ import annotations |
| |
| from pyfory.serialization import Config |
| from pyfory.lib import mmh3 |
| from pyfory.meta.metastring import Encoding |
| from pyfory.resolver import ( |
| MapRefReader, |
| MapRefWriter, |
| NoRefReader, |
| NoRefWriter, |
| NOT_NULL_VALUE_FLAG, |
| NULL_FLAG, |
| ) |
| from pyfory.types import TypeId |
| |
| SMALL_STRING_THRESHOLD = 16 |
| INT64_TYPE_ID = TypeId.VARINT64 |
| FLOAT64_TYPE_ID = TypeId.FLOAT64 |
| BOOL_TYPE_ID = TypeId.BOOL |
| STRING_TYPE_ID = TypeId.STRING |
| |
| |
| def _mix64(x: int) -> int: |
| x &= 0xFFFFFFFFFFFFFFFF |
| x ^= x >> 33 |
| x = (x * 0xFF51AFD7ED558CCD) & 0xFFFFFFFFFFFFFFFF |
| x ^= x >> 33 |
| x = (x * 0xC4CEB9FE1A85EC53) & 0xFFFFFFFFFFFFFFFF |
| x ^= x >> 33 |
| return x |
| |
| |
| def hash_small_metastring(v1: int, v2: int, length: int, encoding: int) -> int: |
| k = 0x9E3779B97F4A7C15 |
| x = (v1 & 0xFFFFFFFFFFFFFFFF) ^ ((v2 & 0xFFFFFFFFFFFFFFFF) * k & 0xFFFFFFFFFFFFFFFF) |
| x ^= (length & 0xFFFFFFFFFFFFFFFF) << 56 |
| h = _mix64(x) |
| return (h & 0xFFFFFFFFFFFFFF00) | (encoding & 0xFF) |
| |
| |
| def hash_meta_string_data(data: bytes, encoding: int) -> int: |
| length = len(data) |
| if length <= SMALL_STRING_THRESHOLD: |
| padded = data + b"\x00" * (16 - length) |
| v1 = int.from_bytes(padded[:8], "little", signed=False) |
| v2 = int.from_bytes(padded[8:16], "little", signed=False) |
| return hash_small_metastring(v1, v2, length, encoding) |
| hashcode = mmh3.hash_buffer(data, seed=47)[0] |
| return (hashcode >> 8 << 8) | (encoding & 0xFF) |
| |
| |
| class EncodedMetaString: |
| __slots__ = ("data", "length", "encoding", "hashcode") |
| |
| def __init__(self, data: bytes, hashcode: int): |
| self.data = data |
| self.length = len(data) |
| if hashcode >= (1 << 63): |
| hashcode -= 1 << 64 |
| elif hashcode < -(1 << 63): |
| hashcode += 1 << 64 |
| self.hashcode = hashcode |
| self.encoding = hashcode & 0xFF |
| |
| def decode(self, decoder): |
| return decoder.decode(self.data, Encoding(self.encoding)) |
| |
| def __hash__(self): |
| return hash((self.hashcode, self.data)) |
| |
| def __eq__(self, other): |
| return type(other) is EncodedMetaString and self.hashcode == other.hashcode and self.data == other.data |
| |
| def __repr__(self): |
| return f"EncodedMetaString(data={self.data}, hashcode={self.hashcode})" |
| |
| |
| EMPTY_ENCODED_META_STRING = EncodedMetaString(b"", 0) |
| |
| |
| class MetaStringWriter: |
| __slots__ = ("_written_meta_strings",) |
| |
| def __init__(self): |
| self._written_meta_strings = {} |
| |
| def write_encoded_meta_string(self, buffer, encoded_meta_string: EncodedMetaString): |
| entry = self._written_meta_strings.get(id(encoded_meta_string)) |
| length = encoded_meta_string.length |
| if entry is None: |
| dynamic_id = len(self._written_meta_strings) |
| self._written_meta_strings[id(encoded_meta_string)] = (dynamic_id, encoded_meta_string) |
| buffer.write_var_uint32(length << 1) |
| if length <= SMALL_STRING_THRESHOLD: |
| if length != 0: |
| buffer.write_int8(encoded_meta_string.encoding) |
| else: |
| buffer.write_int64(encoded_meta_string.hashcode) |
| buffer.write_bytes(encoded_meta_string.data) |
| return |
| buffer.write_var_uint32(((entry[0] + 1) << 1) | 1) |
| |
| def reset(self): |
| self._written_meta_strings.clear() |
| |
| |
| class MetaStringReader: |
| __slots__ = ( |
| "shared_registry", |
| "_dynamic_id_to_encoded_meta_strings", |
| "_hash_to_encoded_meta_strings", |
| "_small_encoded_meta_strings", |
| ) |
| |
| def __init__(self, shared_registry): |
| self.shared_registry = shared_registry |
| self._dynamic_id_to_encoded_meta_strings = [] |
| self._hash_to_encoded_meta_strings = {} |
| self._small_encoded_meta_strings = {} |
| |
| def read_encoded_meta_string(self, buffer): |
| header = buffer.read_var_uint32() |
| length = header >> 1 |
| if (header & 0b1) != 0: |
| if length <= 0: |
| raise ValueError("Invalid dynamic metastring id 0") |
| return self._dynamic_id_to_encoded_meta_strings[length - 1] |
| if length <= SMALL_STRING_THRESHOLD: |
| encoded_meta_string = self._read_small_meta_string(buffer, length) |
| else: |
| encoded_meta_string = self._read_big_meta_string(buffer, length) |
| self._dynamic_id_to_encoded_meta_strings.append(encoded_meta_string) |
| return encoded_meta_string |
| |
| def _read_small_meta_string(self, buffer, length: int): |
| if length == 0: |
| return EMPTY_ENCODED_META_STRING |
| encoding = buffer.read_int8() |
| if length <= 8: |
| v1 = buffer.read_bytes_as_int64(length) |
| v2 = 0 |
| else: |
| v1 = buffer.read_int64() |
| v2 = buffer.read_bytes_as_int64(length - 8) |
| key = (v1, v2, encoding, length) |
| encoded_meta_string = self._small_encoded_meta_strings.get(key) |
| if encoded_meta_string is not None: |
| return encoded_meta_string |
| hashcode = hash_small_metastring(v1, v2, length, encoding) |
| reader_index = buffer.get_reader_index() |
| data = buffer.get_bytes(reader_index - length, length) |
| encoded_meta_string = self.shared_registry.get_or_create_encoded_meta_string(data, hashcode) |
| self._small_encoded_meta_strings[key] = encoded_meta_string |
| return encoded_meta_string |
| |
| def _read_big_meta_string(self, buffer, length: int): |
| hashcode = buffer.read_int64() |
| reader_index = buffer.get_reader_index() |
| buffer.check_bound(reader_index, length) |
| data = buffer.get_bytes(reader_index, length) |
| buffer.set_reader_index(reader_index + length) |
| key = (hashcode, data) |
| encoded_meta_string = self._hash_to_encoded_meta_strings.get(key) |
| if encoded_meta_string is None: |
| encoded_meta_string = self.shared_registry.get_or_create_encoded_meta_string(data, hashcode) |
| self._hash_to_encoded_meta_strings[key] = encoded_meta_string |
| return encoded_meta_string |
| |
| def reset(self): |
| self._dynamic_id_to_encoded_meta_strings.clear() |
| |
| |
| class MetaShareWriteContext: |
| __slots__ = ("class_map",) |
| |
| def __init__(self): |
| self.class_map = {} |
| |
| def reset(self): |
| self.class_map.clear() |
| |
| |
| class MetaShareReadContext: |
| __slots__ = ("read_type_infos",) |
| |
| def __init__(self): |
| self.read_type_infos = [] |
| |
| def reset(self): |
| self.read_type_infos.clear() |
| |
| |
| class WriteContext: |
| __slots__ = ( |
| "type_resolver", |
| "xlang", |
| "track_ref", |
| "strict", |
| "compatible", |
| "field_nullable", |
| "policy", |
| "max_collection_size", |
| "max_binary_size", |
| "ref_writer", |
| "meta_string_writer", |
| "meta_share_context", |
| "buffer", |
| "buffer_callback", |
| "unsupported_callback", |
| "context_objects", |
| "depth", |
| ) |
| |
| def __init__(self, config: Config, type_resolver): |
| self.type_resolver = type_resolver |
| self.xlang = config.xlang |
| self.track_ref = config.track_ref |
| self.strict = config.strict |
| self.compatible = config.compatible |
| self.field_nullable = config.field_nullable |
| self.policy = config.policy |
| self.max_collection_size = config.max_collection_size |
| self.max_binary_size = config.max_binary_size |
| self.ref_writer = MapRefWriter() if self.track_ref else NoRefWriter() |
| self.meta_string_writer = MetaStringWriter() |
| self.meta_share_context = MetaShareWriteContext() if config.scoped_meta_share_enabled else None |
| self.buffer = None |
| self.buffer_callback = None |
| self.unsupported_callback = None |
| self.context_objects = {} |
| self.depth = 0 |
| |
| def __getattr__(self, name): |
| buffer = object.__getattribute__(self, "buffer") |
| if buffer is None: |
| raise AttributeError(name) |
| return getattr(buffer, name) |
| |
| def prepare(self, buffer, buffer_callback=None, unsupported_callback=None): |
| self.buffer = buffer |
| self.buffer_callback = buffer_callback |
| self.unsupported_callback = unsupported_callback |
| self.depth = 0 |
| |
| def reset(self): |
| self.ref_writer.reset() |
| self.meta_string_writer.reset() |
| if self.meta_share_context is not None: |
| self.meta_share_context.reset() |
| if self.context_objects: |
| self.context_objects.clear() |
| self.buffer = None |
| self.buffer_callback = None |
| self.unsupported_callback = None |
| self.depth = 0 |
| |
| def add_context_object(self, key, obj): |
| self.context_objects[id(key)] = obj |
| |
| def has_context_object(self, key) -> bool: |
| return id(key) in self.context_objects |
| |
| def get_context_object(self, key, default=None): |
| return self.context_objects.get(id(key), default) |
| |
| def increase_depth(self, diff=1): |
| self.depth += diff |
| |
| def decrease_depth(self, diff=1): |
| self.depth -= diff |
| |
| def write_ref_or_null(self, obj): |
| return self.ref_writer.write_ref_or_null(self.buffer, obj) |
| |
| def write_ref_value_flag(self, obj): |
| return self.ref_writer.write_ref_value_flag(self.buffer, obj) |
| |
| def write_null_flag(self, obj): |
| return self.ref_writer.write_null_flag(self.buffer, obj) |
| |
| def write_ref(self, obj, typeinfo=None, serializer=None): |
| if serializer is None and typeinfo is not None: |
| serializer = typeinfo.serializer |
| if serializer is None or serializer.need_to_write_ref: |
| if self.ref_writer.write_ref_or_null(self.buffer, obj): |
| return |
| self.write_non_ref(obj, serializer=serializer, typeinfo=typeinfo) |
| return |
| if obj is None: |
| self.buffer.write_int8(NULL_FLAG) |
| return |
| self.buffer.write_int8(NOT_NULL_VALUE_FLAG) |
| self.write_non_ref(obj, serializer=serializer, typeinfo=typeinfo) |
| |
| def write_non_ref(self, obj, serializer=None, typeinfo=None): |
| if serializer is not None: |
| self.increase_depth() |
| serializer.write(self, obj) |
| self.decrease_depth() |
| return |
| cls = type(obj) |
| if cls is str: |
| self.buffer.write_uint8(STRING_TYPE_ID) |
| self.buffer.write_string(obj) |
| return |
| if cls is int: |
| self.buffer.write_uint8(INT64_TYPE_ID) |
| self.buffer.write_varint64(obj) |
| return |
| if cls is bool: |
| self.buffer.write_uint8(BOOL_TYPE_ID) |
| self.buffer.write_bool(obj) |
| return |
| if cls is float: |
| self.buffer.write_uint8(FLOAT64_TYPE_ID) |
| self.buffer.write_double(obj) |
| return |
| if typeinfo is None: |
| typeinfo = self.type_resolver.get_type_info(cls) |
| self.type_resolver.write_type_info(self, typeinfo) |
| self.increase_depth() |
| typeinfo.serializer.write(self, obj) |
| self.decrease_depth() |
| |
| def write_no_ref(self, obj, serializer=None, typeinfo=None): |
| self.write_non_ref(obj, serializer=serializer, typeinfo=typeinfo) |
| |
| def write_buffer_object(self, buffer_object): |
| if self.buffer_callback is None: |
| size = buffer_object.total_bytes() |
| self.buffer.write_var_uint32(size) |
| writer_index = self.buffer.get_writer_index() |
| self.buffer.ensure(writer_index + size) |
| buf = self.buffer.slice(writer_index, size) |
| buffer_object.write_to(buf) |
| self.buffer.set_writer_index(writer_index + size) |
| return |
| if self.buffer_callback(buffer_object): |
| self.buffer.write_bool(True) |
| size = buffer_object.total_bytes() |
| self.buffer.write_var_uint32(size) |
| writer_index = self.buffer.get_writer_index() |
| self.buffer.ensure(writer_index + size) |
| buf = self.buffer.slice(writer_index, size) |
| buffer_object.write_to(buf) |
| self.buffer.set_writer_index(writer_index + size) |
| return |
| self.buffer.write_bool(False) |
| |
| def handle_unsupported_write(self, obj): |
| if self.unsupported_callback is None or self.unsupported_callback(obj): |
| raise NotImplementedError(f"{type(obj)} is not supported for write") |
| |
| def enter_flush_barrier(self): |
| output_stream = None if self.buffer is None else self.buffer.get_output_stream() |
| if output_stream is not None: |
| output_stream.enter_flush_barrier() |
| |
| def exit_flush_barrier(self): |
| output_stream = None if self.buffer is None else self.buffer.get_output_stream() |
| if output_stream is not None: |
| output_stream.exit_flush_barrier() |
| |
| def try_flush(self): |
| if self.buffer is None or self.buffer.get_writer_index() <= 4096: |
| return |
| output_stream = self.buffer.get_output_stream() |
| if output_stream is not None: |
| output_stream.try_flush() |
| |
| def force_flush(self): |
| if self.buffer is None: |
| return |
| output_stream = self.buffer.get_output_stream() |
| if output_stream is not None: |
| output_stream.force_flush() |
| |
| def write_bool(self, value): |
| self.buffer.write_bool(value) |
| |
| def write_int8(self, value): |
| self.buffer.write_int8(value) |
| |
| def write_uint8(self, value): |
| self.buffer.write_uint8(value) |
| |
| def write_int16(self, value): |
| self.buffer.write_int16(value) |
| |
| def write_uint16(self, value): |
| self.buffer.write_uint16(value) |
| |
| def write_int32(self, value): |
| self.buffer.write_int32(value) |
| |
| def write_uint32(self, value): |
| self.buffer.write_uint32(value) |
| |
| def write_int64(self, value): |
| self.buffer.write_int64(value) |
| |
| def write_uint64(self, value): |
| self.buffer.write_uint64(value) |
| |
| def write_varint32(self, value): |
| self.buffer.write_varint32(value) |
| |
| def write_var_uint32(self, value): |
| self.buffer.write_var_uint32(value) |
| |
| def write_varint64(self, value): |
| self.buffer.write_varint64(value) |
| |
| def write_var_uint64(self, value): |
| self.buffer.write_var_uint64(value) |
| |
| def write_tagged_int64(self, value): |
| self.buffer.write_tagged_int64(value) |
| |
| def write_tagged_uint64(self, value): |
| self.buffer.write_tagged_uint64(value) |
| |
| def write_float(self, value): |
| self.buffer.write_float(value) |
| |
| def write_double(self, value): |
| self.buffer.write_double(value) |
| |
| def write_string(self, value): |
| self.buffer.write_string(value) |
| |
| def write_bytes(self, value): |
| self.buffer.write_bytes(value) |
| |
| |
| class ReadContext: |
| __slots__ = ( |
| "type_resolver", |
| "xlang", |
| "track_ref", |
| "strict", |
| "compatible", |
| "field_nullable", |
| "policy", |
| "max_collection_size", |
| "max_binary_size", |
| "max_depth", |
| "ref_reader", |
| "meta_string_reader", |
| "meta_share_context", |
| "buffer", |
| "buffers", |
| "unsupported_objects", |
| "peer_out_of_band_enabled", |
| "context_objects", |
| "depth", |
| ) |
| |
| def __init__(self, config: Config, type_resolver): |
| self.type_resolver = type_resolver |
| self.xlang = config.xlang |
| self.track_ref = config.track_ref |
| self.strict = config.strict |
| self.compatible = config.compatible |
| self.field_nullable = config.field_nullable |
| self.policy = config.policy |
| self.max_collection_size = config.max_collection_size |
| self.max_binary_size = config.max_binary_size |
| self.max_depth = config.max_depth |
| self.ref_reader = MapRefReader() if self.track_ref else NoRefReader() |
| self.meta_string_reader = MetaStringReader(type_resolver.shared_registry) |
| self.meta_share_context = MetaShareReadContext() if config.scoped_meta_share_enabled else None |
| self.buffer = None |
| self.buffers = None |
| self.unsupported_objects = None |
| self.peer_out_of_band_enabled = False |
| self.context_objects = {} |
| self.depth = 0 |
| |
| def __getattr__(self, name): |
| buffer = object.__getattribute__(self, "buffer") |
| if buffer is None: |
| raise AttributeError(name) |
| return getattr(buffer, name) |
| |
| def prepare( |
| self, |
| buffer, |
| buffers=None, |
| unsupported_objects=None, |
| peer_out_of_band_enabled=False, |
| ): |
| self.buffer = buffer |
| self.buffers = iter(buffers) if buffers is not None else None |
| self.unsupported_objects = iter(unsupported_objects) if unsupported_objects is not None else None |
| self.peer_out_of_band_enabled = peer_out_of_band_enabled |
| self.depth = 0 |
| |
| def reset(self): |
| self.ref_reader.reset() |
| self.meta_string_reader.reset() |
| if self.meta_share_context is not None: |
| self.meta_share_context.reset() |
| if self.context_objects: |
| self.context_objects.clear() |
| self.buffer = None |
| self.buffers = None |
| self.unsupported_objects = None |
| self.peer_out_of_band_enabled = False |
| self.depth = 0 |
| |
| def add_context_object(self, key, obj): |
| self.context_objects[id(key)] = obj |
| |
| def has_context_object(self, key) -> bool: |
| return id(key) in self.context_objects |
| |
| def get_context_object(self, key, default=None): |
| return self.context_objects.get(id(key), default) |
| |
| def increase_depth(self, diff=1): |
| # Depth accounting is paired on the successful path only. |
| # If a nested read raises, the top-level deserialize/reset path clears |
| # `depth`, so nested readers must not add local try/finally wrappers |
| # around increase/decrease pairs. |
| self.depth += diff |
| if self.depth > self.max_depth: |
| raise Exception( |
| f"Read depth exceed max depth: {self.depth}, the deserialization data may be malicious. If it's not malicious, " |
| "please increase max read depth by Fory(..., max_depth=...)" |
| ) |
| |
| def decrease_depth(self, diff=1): |
| # Only call this after the matching nested read completed successfully. |
| self.depth -= diff |
| |
| def read_ref_or_null(self): |
| return self.ref_reader.read_ref_or_null(self.buffer) |
| |
| def preserve_ref_id(self, ref_id=None): |
| return self.ref_reader.preserve_ref_id(ref_id) |
| |
| def try_preserve_ref_id(self): |
| return self.ref_reader.try_preserve_ref_id(self.buffer) |
| |
| def last_preserved_ref_id(self): |
| return self.ref_reader.last_preserved_ref_id() |
| |
| def has_preserved_ref_id(self): |
| return self.ref_reader.has_preserved_ref_id() |
| |
| def reference(self, obj): |
| self.ref_reader.reference(obj) |
| |
| def get_read_ref(self, ref_id=None): |
| return self.ref_reader.get_read_ref(ref_id) |
| |
| def set_read_ref(self, ref_id, obj): |
| self.ref_reader.set_read_ref(ref_id, obj) |
| |
| def read_ref(self, serializer=None): |
| if serializer is None or serializer.need_to_write_ref: |
| ref_id = self.ref_reader.try_preserve_ref_id(self.buffer) |
| if ref_id >= NOT_NULL_VALUE_FLAG: |
| obj = self._read_non_ref_internal(serializer) |
| self.ref_reader.set_read_ref(ref_id, obj) |
| return obj |
| return self.ref_reader.get_read_ref() |
| head_flag = self.buffer.read_int8() |
| if head_flag == NULL_FLAG: |
| return None |
| return self.read_non_ref(serializer=serializer) |
| |
| def read_non_ref(self, serializer=None): |
| if self.track_ref and (serializer is None or serializer.need_to_write_ref): |
| self.ref_reader.preserve_ref_id(-1) |
| return self._read_non_ref_internal(serializer) |
| |
| def read_no_ref(self, serializer=None): |
| return self.read_non_ref(serializer=serializer) |
| |
| def read_nullable(self, serializer=None): |
| if self.buffer.read_int8() == NULL_FLAG: |
| return None |
| return self.read_non_ref(serializer=serializer) |
| |
| def _read_non_ref_internal(self, serializer=None): |
| if serializer is None: |
| typeinfo = self.type_resolver.read_type_info(self) |
| type_id = typeinfo.type_id |
| if type_id == STRING_TYPE_ID: |
| return self.buffer.read_string() |
| if type_id == INT64_TYPE_ID: |
| return self.buffer.read_varint64() |
| if type_id == BOOL_TYPE_ID: |
| return self.buffer.read_bool() |
| if type_id == FLOAT64_TYPE_ID: |
| return self.buffer.read_double() |
| serializer = typeinfo.serializer |
| self.increase_depth() |
| obj = serializer.read(self) |
| self.decrease_depth() |
| return obj |
| |
| def read_buffer_object(self): |
| if not self.peer_out_of_band_enabled: |
| size = self.buffer.read_var_uint32() |
| if self.buffer.has_input_stream(): |
| return self.buffer.read_bytes(size) |
| reader_index = self.buffer.get_reader_index() |
| buf = self.buffer.slice(reader_index, size) |
| self.buffer.set_reader_index(reader_index + size) |
| return buf |
| in_band = self.buffer.read_bool() |
| if not in_band: |
| assert self.buffers is not None |
| return next(self.buffers) |
| size = self.buffer.read_var_uint32() |
| if self.buffer.has_input_stream(): |
| return self.buffer.read_bytes(size) |
| reader_index = self.buffer.get_reader_index() |
| buf = self.buffer.slice(reader_index, size) |
| self.buffer.set_reader_index(reader_index + size) |
| return buf |
| |
| def handle_unsupported_read(self): |
| assert self.unsupported_objects is not None |
| return next(self.unsupported_objects) |
| |
| def read_bool(self): |
| return self.buffer.read_bool() |
| |
| def read_int8(self): |
| return self.buffer.read_int8() |
| |
| def read_uint8(self): |
| return self.buffer.read_uint8() |
| |
| def read_int16(self): |
| return self.buffer.read_int16() |
| |
| def read_uint16(self): |
| return self.buffer.read_uint16() |
| |
| def read_int32(self): |
| return self.buffer.read_int32() |
| |
| def read_uint32(self): |
| return self.buffer.read_uint32() |
| |
| def read_int64(self): |
| return self.buffer.read_int64() |
| |
| def read_uint64(self): |
| return self.buffer.read_uint64() |
| |
| def read_varint32(self): |
| return self.buffer.read_varint32() |
| |
| def read_var_uint32(self): |
| return self.buffer.read_var_uint32() |
| |
| def read_varint64(self): |
| return self.buffer.read_varint64() |
| |
| def read_var_uint64(self): |
| return self.buffer.read_var_uint64() |
| |
| def read_tagged_int64(self): |
| return self.buffer.read_tagged_int64() |
| |
| def read_tagged_uint64(self): |
| return self.buffer.read_tagged_uint64() |
| |
| def read_float(self): |
| return self.buffer.read_float() |
| |
| def read_double(self): |
| return self.buffer.read_double() |
| |
| def read_string(self): |
| return self.buffer.read_string() |