flow.record 3.22.dev3__tar.gz → 3.22.dev5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/PKG-INFO +1 -1
  2. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/base.py +2 -0
  3. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/jsonpacker.py +14 -7
  4. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/stream.py +7 -2
  5. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/version.py +3 -3
  6. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow.record.egg-info/PKG-INFO +1 -1
  7. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow.record.egg-info/SOURCES.txt +1 -0
  8. flow_record-3.22.dev5/tests/fieldtypes/test_boolean.py +35 -0
  9. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/fieldtypes/test_fieldtypes.py +0 -30
  10. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/packer/test_json_packer.py +10 -0
  11. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/record/test_adapter.py +8 -0
  12. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/tools/test_rdump.py +73 -0
  13. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/.git-blame-ignore-revs +0 -0
  14. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/.gitattributes +0 -0
  15. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/COPYRIGHT +0 -0
  16. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/LICENSE +0 -0
  17. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/MANIFEST.in +0 -0
  18. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/README.md +0 -0
  19. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/examples/__init__.py +0 -0
  20. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/examples/filesystem.py +0 -0
  21. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/examples/passivedns.py +0 -0
  22. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/examples/records.json +0 -0
  23. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/examples/selectors.py +0 -0
  24. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/examples/tcpconn.py +0 -0
  25. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/__init__.py +0 -0
  26. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/adapter/__init__.py +0 -0
  27. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/adapter/archive.py +0 -0
  28. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/adapter/avro.py +0 -0
  29. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/adapter/broker.py +0 -0
  30. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/adapter/csvfile.py +0 -0
  31. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/adapter/duckdb.py +0 -0
  32. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/adapter/elastic.py +0 -0
  33. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/adapter/jsonfile.py +0 -0
  34. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/adapter/line.py +0 -0
  35. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/adapter/mongo.py +0 -0
  36. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/adapter/split.py +0 -0
  37. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/adapter/splunk.py +0 -0
  38. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/adapter/sqlite.py +0 -0
  39. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/adapter/stream.py +0 -0
  40. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/adapter/text.py +0 -0
  41. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/adapter/xlsx.py +0 -0
  42. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/context.py +0 -0
  43. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/exceptions.py +0 -0
  44. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/fieldtypes/__init__.py +0 -0
  45. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/fieldtypes/credential.py +0 -0
  46. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/fieldtypes/net/__init__.py +0 -0
  47. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/fieldtypes/net/ip.py +0 -0
  48. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/fieldtypes/net/ipv4.py +0 -0
  49. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/fieldtypes/net/tcp.py +0 -0
  50. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/fieldtypes/net/udp.py +0 -0
  51. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/packer.py +0 -0
  52. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/selector.py +0 -0
  53. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/tools/__init__.py +0 -0
  54. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/tools/geoip.py +0 -0
  55. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/tools/rdump.py +0 -0
  56. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/utils.py +0 -0
  57. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow/record/whitelist.py +0 -0
  58. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow.record.egg-info/dependency_links.txt +0 -0
  59. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow.record.egg-info/entry_points.txt +0 -0
  60. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow.record.egg-info/requires.txt +0 -0
  61. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/flow.record.egg-info/top_level.txt +0 -0
  62. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/pyproject.toml +0 -0
  63. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/setup.cfg +0 -0
  64. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/__init__.py +0 -0
  65. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/_data/.gitkeep +0 -0
  66. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/_docs/Makefile +0 -0
  67. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/_docs/conf.py +0 -0
  68. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/_docs/index.rst +0 -0
  69. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/_utils.py +0 -0
  70. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/adapter/__init__.py +0 -0
  71. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/adapter/test_avro.py +0 -0
  72. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/adapter/test_csv.py +0 -0
  73. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/adapter/test_elastic.py +0 -0
  74. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/adapter/test_json.py +0 -0
  75. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/adapter/test_line.py +0 -0
  76. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/adapter/test_splunk.py +0 -0
  77. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/adapter/test_sqlite_duckdb.py +0 -0
  78. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/adapter/test_text.py +0 -0
  79. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/adapter/test_xlsx.py +0 -0
  80. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/conftest.py +0 -0
  81. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/fieldtypes/__init__.py +0 -0
  82. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/fieldtypes/test_ip.py +0 -0
  83. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/packer/__init__.py +0 -0
  84. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/packer/test_packer.py +0 -0
  85. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/record/__init__.py +0 -0
  86. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/record/test_context.py +0 -0
  87. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/record/test_descriptor.py +0 -0
  88. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/record/test_multi_timestamp.py +0 -0
  89. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/record/test_record.py +0 -0
  90. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/selector/__init__.py +0 -0
  91. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/selector/test_compiled.py +0 -0
  92. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/selector/test_selectors.py +0 -0
  93. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/test_deprecations.py +0 -0
  94. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/test_regressions.py +0 -0
  95. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/test_utils.py +0 -0
  96. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tests/tools/__init__.py +0 -0
  97. {flow_record-3.22.dev3 → flow_record-3.22.dev5}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flow.record
3
- Version: 3.22.dev3
3
+ Version: 3.22.dev5
4
4
  Summary: A library for defining and creating structured data (called records) that can be streamed to disk or piped to other tools that use flow.record
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -893,6 +893,8 @@ def RecordAdapter(
893
893
  "entering record text, rather than a record stream? This can be fixed by using "
894
894
  "'rdump -w -' to write a record stream to stdout."
895
895
  )
896
+ if not peek_data:
897
+ raise EOFError("Empty input stream")
896
898
  raise RecordAdapterNotFound("Could not find adapter for file-like object")
897
899
 
898
900
  # Now that we found an adapter, we will fall back into the same code path as when a URL is given. As the url
@@ -49,12 +49,7 @@ class JsonRecordPacker:
49
49
  serial["_type"] = "record"
50
50
  serial["_recorddescriptor"] = obj._desc.identifier
51
51
 
52
- for field_type, field_name in obj._desc.get_field_tuples():
53
- # Boolean field types should be cast to a bool instead of staying ints
54
- if field_type == "boolean" and isinstance(serial[field_name], int):
55
- serial[field_name] = bool(serial[field_name])
56
-
57
- return serial
52
+ return self.convert_basic_types(serial)
58
53
  if isinstance(obj, RecordDescriptor):
59
54
  return {
60
55
  "_type": "recorddescriptor",
@@ -102,7 +97,19 @@ class JsonRecordPacker:
102
97
  return RecordDescriptor._unpack(*data)
103
98
  return obj
104
99
 
105
- def pack(self, obj: Record | RecordDescriptor) -> str:
100
+ def convert_basic_types(self, obj: Any) -> Any:
101
+ """Explicitly convert some basic types when packing to JSON."""
102
+ if isinstance(obj, fieldtypes.boolean):
103
+ return bool(obj)
104
+ if isinstance(obj, dict):
105
+ return {k: self.convert_basic_types(v) for k, v in obj.items()}
106
+ if isinstance(obj, list):
107
+ return [self.convert_basic_types(item) for item in obj]
108
+ return obj
109
+
110
+ def pack(self, obj: Record | RecordDescriptor | dict) -> str:
111
+ if isinstance(obj, dict):
112
+ obj = self.convert_basic_types(obj)
106
113
  return json.dumps(obj, default=self.pack_obj, indent=self.indent)
107
114
 
108
115
  def unpack(self, d: str) -> RecordDescriptor | Record:
@@ -164,11 +164,13 @@ def record_stream(sources: list[str], selector: str | None = None) -> Iterator[R
164
164
  print("[reading from stdin]", file=sys.stderr)
165
165
 
166
166
  # Initial value for reader, in case of exception message
167
- reader = "RecordReader"
167
+ reader: str | AbstractReader = "RecordReader"
168
168
  try:
169
169
  reader = RecordReader(src, selector=selector)
170
170
  yield from reader
171
- reader.close()
171
+ except EOFError as e:
172
+ # End of file reached, likely no records in source
173
+ log.warning("%s(%r): %s", reader, src, e)
172
174
  except IOError as e:
173
175
  if len(sources) == 1:
174
176
  raise
@@ -184,6 +186,9 @@ def record_stream(sources: list[str], selector: str | None = None) -> Iterator[R
184
186
  else:
185
187
  log.warning("Exception in %r for %r: %s -- skipping to next reader", reader, src, aRepr.repr(e))
186
188
  continue
189
+ finally:
190
+ if isinstance(reader, AbstractReader):
191
+ reader.close()
187
192
 
188
193
 
189
194
  class PathTemplateWriter:
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '3.22.dev3'
32
- __version_tuple__ = version_tuple = (3, 22, 'dev3')
31
+ __version__ = version = '3.22.dev5'
32
+ __version_tuple__ = version_tuple = (3, 22, 'dev5')
33
33
 
34
- __commit_id__ = commit_id = 'g6fab237c7'
34
+ __commit_id__ = commit_id = 'gc3f8cd8c6'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flow.record
3
- Version: 3.22.dev3
3
+ Version: 3.22.dev5
4
4
  Summary: A library for defining and creating structured data (called records) that can be streamed to disk or piped to other tools that use flow.record
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -76,6 +76,7 @@ tests/adapter/test_sqlite_duckdb.py
76
76
  tests/adapter/test_text.py
77
77
  tests/adapter/test_xlsx.py
78
78
  tests/fieldtypes/__init__.py
79
+ tests/fieldtypes/test_boolean.py
79
80
  tests/fieldtypes/test_fieldtypes.py
80
81
  tests/fieldtypes/test_ip.py
81
82
  tests/packer/__init__.py
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from flow.record.base import RecordDescriptor
6
+
7
+
8
+ def test_boolean() -> None:
9
+ TestRecord = RecordDescriptor(
10
+ "test/boolean",
11
+ [
12
+ ("boolean", "booltrue"),
13
+ ("boolean", "boolfalse"),
14
+ ],
15
+ )
16
+
17
+ r = TestRecord(True, False)
18
+ assert bool(r.booltrue) is True
19
+ assert bool(r.boolfalse) is False
20
+
21
+ r = TestRecord(1, 0)
22
+ assert bool(r.booltrue) is True
23
+ assert bool(r.boolfalse) is False
24
+
25
+ assert str(r.booltrue) == "True"
26
+ assert str(r.boolfalse) == "False"
27
+
28
+ assert repr(r.booltrue) == "True"
29
+ assert repr(r.boolfalse) == "False"
30
+
31
+ with pytest.raises(ValueError, match="Value not a valid boolean value"):
32
+ TestRecord(2, -1)
33
+
34
+ with pytest.raises(ValueError, match="invalid literal for int"):
35
+ TestRecord("True", "False")
@@ -293,36 +293,6 @@ def test_dictlist() -> None:
293
293
  assert r.hits[1]["b"] == 4
294
294
 
295
295
 
296
- def test_boolean() -> None:
297
- TestRecord = RecordDescriptor(
298
- "test/boolean",
299
- [
300
- ("boolean", "booltrue"),
301
- ("boolean", "boolfalse"),
302
- ],
303
- )
304
-
305
- r = TestRecord(True, False)
306
- assert bool(r.booltrue) is True
307
- assert bool(r.boolfalse) is False
308
-
309
- r = TestRecord(1, 0)
310
- assert bool(r.booltrue) is True
311
- assert bool(r.boolfalse) is False
312
-
313
- assert str(r.booltrue) == "True"
314
- assert str(r.boolfalse) == "False"
315
-
316
- assert repr(r.booltrue) == "True"
317
- assert repr(r.boolfalse) == "False"
318
-
319
- with pytest.raises(ValueError, match="Value not a valid boolean value"):
320
- r = TestRecord(2, -1)
321
-
322
- with pytest.raises(ValueError, match="invalid literal for int"):
323
- r = TestRecord("True", "False")
324
-
325
-
326
296
  def test_float() -> None:
327
297
  TestRecord = RecordDescriptor(
328
298
  "test/float",
@@ -93,6 +93,16 @@ def test_record_pack_bool_regression() -> None:
93
93
  # pack the json string back to a record and make sure it is the same as before
94
94
  assert packer.unpack(data) == record
95
95
 
96
+ # Make sure the same applies to an OrderedDict, which is how JsonRecordPacker is invoked for
97
+ # the Elastic adapter.
98
+ rdict = record._asdict()
99
+ data = packer.pack(rdict)
100
+ assert data.startswith('{"some_varint": 1, "some_uint": 0, "some_boolean": false, ')
101
+
102
+ # test that packer.pack has no side effects on rdict
103
+ assert rdict == record._asdict()
104
+ assert isinstance(rdict["some_boolean"], fieldtypes.boolean)
105
+
96
106
 
97
107
  def test_record_pack_surrogateescape() -> None:
98
108
  TestRecord = RecordDescriptor(
@@ -499,3 +499,11 @@ def test_file_like_writer_reader() -> None:
499
499
  assert len(read_records) == 10
500
500
  for idx, record in enumerate(read_records):
501
501
  assert record == test_records[idx]
502
+
503
+
504
+ def test_empty_stdin(monkeypatch: pytest.MonkeyPatch) -> None:
505
+ # Mock stdin to be empty
506
+ monkeypatch.setattr(sys, "stdin", BytesIO(b""))
507
+
508
+ with pytest.raises(EOFError, match="Empty input stream"):
509
+ RecordAdapter()
@@ -797,3 +797,76 @@ def test_rdump_catch_sigpipe(tmp_path: Path) -> None:
797
797
  assert "test/record count=0" in stdout
798
798
  assert "test/record count=1" in stdout
799
799
  assert len(stdout.splitlines()) == 2
800
+
801
+
802
+ def test_rdump_empty_records_pipe(tmp_path: Path) -> None:
803
+ """Test that rdump handles empty records as input gracefully."""
804
+
805
+ # create an empty records file
806
+ path = tmp_path / "empty.records"
807
+ with RecordWriter(path):
808
+ pass
809
+
810
+ # although the records file is empty, it should exist and have a RECORDSTREAM header
811
+ assert path.exists()
812
+ assert b"RECORDSTREAM" in path.read_bytes()
813
+
814
+ # rdump empty.records | rdump -l
815
+ p1 = subprocess.Popen(["rdump", str(path)], stdout=subprocess.PIPE)
816
+ p2 = subprocess.Popen(
817
+ ["rdump", "-l"],
818
+ stdin=p1.stdout,
819
+ stdout=subprocess.PIPE,
820
+ stderr=subprocess.PIPE,
821
+ )
822
+ stdout, stderr = p2.communicate()
823
+ assert p2.returncode == 0
824
+ assert b"RecordReader('-'): Empty input stream" in stderr
825
+ assert b"Processed 0 records (matched=0, unmatched=0)" in stdout
826
+
827
+
828
+ @pytest.mark.parametrize(
829
+ "stdin_bytes",
830
+ [
831
+ b"",
832
+ None,
833
+ ],
834
+ )
835
+ def test_rdump_empty_stdin_pipe(stdin_bytes: bytes | None) -> None:
836
+ """Test that rdump handles empty stdin as input gracefully."""
837
+
838
+ # rdump -l (with empty stdin)
839
+ pipe = subprocess.Popen(
840
+ ["rdump", "-l"],
841
+ stdin=subprocess.PIPE,
842
+ stdout=subprocess.PIPE,
843
+ stderr=subprocess.PIPE,
844
+ )
845
+ stdout, stderr = pipe.communicate(input=None)
846
+ assert pipe.returncode == 0
847
+ assert b"RecordReader('-'): Empty input stream" in stderr
848
+ assert b"Processed 0 records (matched=0, unmatched=0)" in stdout
849
+
850
+
851
+ @pytest.mark.parametrize(
852
+ "stdin_bytes",
853
+ [
854
+ b"\n",
855
+ b"this is not a valid record stream",
856
+ b"RANDOMDATA",
857
+ ],
858
+ )
859
+ def test_rdump_invalid_stdin_pipe(stdin_bytes: bytes) -> None:
860
+ """Test that rdump handles invalid stdin as an error"""
861
+
862
+ # rdump -l (with invalid stdin)
863
+ pipe = subprocess.Popen(
864
+ ["rdump", "-l"],
865
+ stdin=subprocess.PIPE,
866
+ stdout=subprocess.PIPE,
867
+ stderr=subprocess.PIPE,
868
+ )
869
+ stdout, stderr = pipe.communicate(input=stdin_bytes)
870
+ assert pipe.returncode == 1, "rdump should exit with error code 1 on invalid input"
871
+ assert b"rdump encountered a fatal error: Could not find adapter for file-like object" in stderr
872
+ assert b"Processed 0 records (matched=0, unmatched=0)" in stdout
File without changes
File without changes