diff --git a/docs/source/pcapkit/foundation/traceflow/tcp.rst b/docs/source/pcapkit/foundation/traceflow/tcp.rst index 70ccaca55d..dba740148d 100644 --- a/docs/source/pcapkit/foundation/traceflow/tcp.rst +++ b/docs/source/pcapkit/foundation/traceflow/tcp.rst @@ -120,7 +120,8 @@ Terminology (tuple) index |--> (Info) data - | |--> 'fpout' : (Optional[str]) output filename if exists + | |--> 'fpout' : (Optional[str]) output filename, or None when + | | the format writes no file | |--> 'index': (tuple) tuple of frame index, both directions, | | in capture order | | |--> (int) frame index diff --git a/pcapkit/foundation/traceflow/data/tcp.py b/pcapkit/foundation/traceflow/data/tcp.py index 6c34815a5d..a04afd52ba 100644 --- a/pcapkit/foundation/traceflow/data/tcp.py +++ b/pcapkit/foundation/traceflow/data/tcp.py @@ -167,7 +167,8 @@ class Index(DeferredPacket, Info): #: :class:`~pcapkit.corekit.packet.DeferredPacket` to resolve. __deferred__ = Deferred - #: Output filename if exists. + #: Path of the flow's output file, or :data:`None` when the output format + #: writes no file. Open and finalised flows report the same path. fpout: 'Optional[str]' #: Tuple of frame index, **both directions**, in capture order. index: 'tuple[int, ...]' diff --git a/pcapkit/foundation/traceflow/tcp.py b/pcapkit/foundation/traceflow/tcp.py index 6a2553fafb..9bf1a280a4 100644 --- a/pcapkit/foundation/traceflow/tcp.py +++ b/pcapkit/foundation/traceflow/tcp.py @@ -436,6 +436,25 @@ def _make_segment(self, packet: 'Packet[_AT]') -> 'Any': timestamp=packet.timestamp, # capture timestamp ) + def _output_path(self, label: 'str') -> 'Optional[str]': + """Path of a flow's output file, as an index entry reports it. + + Arguments: + label: the flow's label + + Returns: + The file :meth:`trace` writes the flow to, or :data:`None` when the + output format writes no file at all. An *empty* extension still + writes one, named after the label alone. + + Both :meth:`submit` and :meth:`_finalise` report through this, so an + open flow and a finalised one name the same file (:issue:`2237`). + + """ + if self._fdpext is None: + return None + return f'{self._fproot}/{label}{self._fdpext}' + @staticmethod def _ended(buffer: 'Buffer[_AT]') -> 'bool': """Whether a flow's connection has been seen to end. @@ -478,7 +497,7 @@ def _finalise(self, bufid: 'BufferID') -> 'Index': logger.debug('TCP flow %s finalised after %d frame(s) (%d forward, %d reverse)', label, len(buf.index), len(buf.forward), len(buf.reverse)) index = Index( - fpout=f'{self._fproot}/{label}{self._fdpext}' if self._fdpext is not None else None, + fpout=self._output_path(label), index=tuple(buf.index), label=label, forward=tuple(buf.forward), @@ -550,7 +569,7 @@ def submit(self) -> 'tuple[Index, ...]': ret = [] # type: list[Index] for buf in self._buffer.values(): - ret.append(Index(fpout=f"{self._fproot}/{buf.label}{self._fdpext}" if self._fdpext else None, + ret.append(Index(fpout=self._output_path(buf.label), index=tuple(buf.index), label=buf.label, forward=tuple(buf.forward), diff --git a/tests/foundation/traceflow/test_tcp.py b/tests/foundation/traceflow/test_tcp.py index 5d897d993a..634f3e4c2b 100644 --- a/tests/foundation/traceflow/test_tcp.py +++ b/tests/foundation/traceflow/test_tcp.py @@ -552,6 +552,35 @@ def test_tcp_trace_ipv6_label_and_no_extension_output_path(self) -> None: self.assertEqual(final_index.index, (1, 2)) self.assertFalse(pathlib.Path(tempdir, f'{label}.None').exists()) + def test_an_empty_extension_reports_the_written_file_open_or_finalised(self) -> None: + """An empty extension still writes a file, and every report names it (#2237). + + The flow file is named after the label alone. :meth:`TCP.submit` reported + :data:`None` for a flow still open while :meth:`TCP._finalise` reported the + path, so one flow's ``fpout`` depended on when it was read. + + """ + from dictdumper import JSON + + from pcapkit.foundation.traceflow.tcp import TCP + + TCP.register_dumper('unit-noext', JSON, '') + + with tempfile.TemporaryDirectory() as tempdir: + trace = TCP(tempdir, 'unit-noext') + label = trace.trace(self._packet(index=1, syn=True)) + expected = f'{tempdir}/{label}' + self.assertTrue(pathlib.Path(expected).is_file()) + + # mid-capture: the flow is open, reported by ``submit`` + open_index, = trace.index + self.assertEqual(open_index.fpout, expected) + + # end of capture: the same flow, reported by ``_finalise`` + trace.finish() + final_index, = trace.index + self.assertEqual(final_index.fpout, expected) + if __name__ == '__main__': unittest.main()