Skip to content

Commit a262223

Browse files
committed
Correctly parse CIP BOOL in all Python version
1 parent 852ec09 commit a262223

File tree

3 files changed

+57
-6
lines changed

3 files changed

+57
-6
lines changed

server/enip/parser.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,7 @@ class words( words_base, cpppo.state ):
143143
#
144144
# USINT -- Parse an 8-bit EtherNet/IP unsigned int
145145
# USINT.produce -- and convert a value back to a 8-bit EtherNet/IP unsigned int
146-
# INT -- Parse a 16-bit EtherNet/IP signed int
147-
# UINT -- Parse a 16-bit EtherNet/IP unsigned int
148-
# DINT -- Parse a 32-bit EtherNet/IP signed int
149-
# UDINT -- Parse a 32-bit EtherNet/IP unsigned int
146+
# ...
150147
#
151148
# You must provide either a name or a context; if you provide neither, then both default to the
152149
# name of the class. An instance of any of these types "is" a parser state machine, and has a
@@ -172,10 +169,29 @@ def produce( cls, value ):
172169
return struct.pack( cls.struct_format, value )
173170

174171
class BOOL( TYPE ):
172+
"""An EtherNet/IP BOOL; 8-bit boolean
173+
174+
Surprisingly, the struct '?' format does *not* work as you might expect. the value b'\x00'
175+
converts to False reliably. However, only b'\x01' converts to True reliably; any other value
176+
may or may not result in True, depending on the version of Python being used! Python 2.7/3.9
177+
return False for eg. b'\x02', while Pypy 3.6.9 return True.
178+
179+
Therefore, we will convert octets to unsigned integer via 'B', and then post-process to bool.
180+
"""
175181
tag_type = 0x00c1 # 193
176-
struct_format = '?'
182+
struct_format = 'B' # do not use '?'!
177183
struct_calcsize = struct.calcsize( struct_format )
178184

185+
def terminate( self, exception, machine=None, path=None, data=None ):
186+
super( BOOL, self ).terminate( exception=exception, machine=machine, path=path, data=data )
187+
if exception is not None:
188+
return
189+
ours = self.context( path=path )
190+
if cpppo.is_listlike( data[ours] ):
191+
data[ours] = list( map( bool, data[ours] ))
192+
else:
193+
data[ours] = bool( data[ours] )
194+
179195
@classmethod
180196
def produce( cls, value ):
181197
"""Historically, a 0xFF has been used to represent an EtherNet/IP CIP BOOL Truthy value."""

server/enip_test.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,41 @@ def test_enip_TYPES_numeric():
301301
assert data.typed_data.data == [2**63]
302302

303303

304+
def test_enip_TYPES_bool():
305+
"""Disappointingly, the struct '?' format in Python2 """
306+
pkt = b'\x00\x01\x02\x04\x08\x10\x20\x40\x80\xff\x00'
307+
pkt_truths = [ False, True, True, True, True, True, True, True, True, True, False ]
308+
data = cpppo.dotdict()
309+
source = cpppo.chainable( pkt )
310+
with enip.BOOL() as machine:
311+
for i,(m,s) in enumerate( machine.run( source=source, data=data )):
312+
log.info( "%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r", m.name_centered(),
313+
i, s, source.sent, source.peek(), data )
314+
assert i == 0
315+
assert data.BOOL == False
316+
317+
data = cpppo.dotdict()
318+
with enip.BOOL() as machine:
319+
for i,(m,s) in enumerate( machine.run( source=source, data=data )):
320+
log.info( "%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r", m.name_centered(),
321+
i, s, source.sent, source.peek(), data )
322+
assert i == 0
323+
assert data.BOOL == True
324+
325+
data = cpppo.dotdict()
326+
source = cpppo.chainable( pkt )
327+
with enip.typed_data( tag_type=enip.BOOL.tag_type, terminal=True ) as machine:
328+
for i,(m,s) in enumerate( machine.run( source=source, data=data )):
329+
log.info( "%s #%3d -> %10.10s; next byte %3d: %-10.10r: %r", m.name_centered(),
330+
i, s, source.sent, source.peek(), data )
331+
assert i == 48
332+
assert data.typed_data.data == pkt_truths
333+
334+
pkt_produced = b'\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00'
335+
enip.typed_data.produce( {'data': pkt_truths}, tag_type=enip.BOOL.tag_type ) == pkt_produced
336+
337+
338+
304339
# pkt4
305340
# "4","0.000863000","192.168.222.128","10.220.104.180","ENIP","82","Register Session (Req)"
306341
rss_004_request = bytes(bytearray([

version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
__version_info__ = ( 4, 3, 2 )
1+
__version_info__ = ( 4, 3, 4 )
22
__version__ = '.'.join( map( str, __version_info__ ))

0 commit comments

Comments
 (0)