Import the bindata and packetfu libraries (thanks Tod)
git-svn-id: file:///home/svn/framework3/trunk@5727 4d416f70-5f16-0410-b530-b9f4589650daunstable
parent
137b9c6cfb
commit
10619f3af0
|
@ -0,0 +1,22 @@
|
|||
# BinData -- Binary data manipulator.
|
||||
# Copyright (c) 2007,2008 Dion Mendel.
|
||||
|
||||
require 'bindata/array'
|
||||
require 'bindata/bits'
|
||||
require 'bindata/choice'
|
||||
require 'bindata/float'
|
||||
require 'bindata/int'
|
||||
require 'bindata/multi_value'
|
||||
require 'bindata/rest'
|
||||
require 'bindata/single_value'
|
||||
require 'bindata/string'
|
||||
require 'bindata/stringz'
|
||||
require 'bindata/struct'
|
||||
|
||||
# = BinData
|
||||
#
|
||||
# A declarative way to read and write structured binary data.
|
||||
#
|
||||
module BinData
|
||||
VERSION = "0.9.2-eofpatch" # Temporary fork for PacketFu.
|
||||
end
|
|
@ -0,0 +1,339 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
|
@ -0,0 +1,52 @@
|
|||
BinData is copyrighted free software by Dion Mendel <dion@lostrealm.com>.
|
||||
You can redistribute it and/or modify it under either the terms of the GPL
|
||||
version 2 (see the file GPL), or the conditions below:
|
||||
|
||||
1. You may make and give away verbatim copies of the source form of the
|
||||
software without restriction, provided that you duplicate all of the
|
||||
original copyright notices and associated disclaimers.
|
||||
|
||||
2. You may modify your copy of the software in any way, provided that
|
||||
you do at least ONE of the following:
|
||||
|
||||
a) place your modifications in the Public Domain or otherwise
|
||||
make them Freely Available, such as by posting said
|
||||
modifications to Usenet or an equivalent medium, or by allowing
|
||||
the author to include your modifications in the software.
|
||||
|
||||
b) use the modified software only within your corporation or
|
||||
organization.
|
||||
|
||||
c) give non-standard binaries non-standard names, with
|
||||
instructions on where to get the original software distribution.
|
||||
|
||||
d) make other distribution arrangements with the author.
|
||||
|
||||
3. You may distribute the software in object code or binary form,
|
||||
provided that you do at least ONE of the following:
|
||||
|
||||
a) distribute the binaries and library files of the software,
|
||||
together with instructions (in the manual page or equivalent)
|
||||
on where to get the original distribution.
|
||||
|
||||
b) accompany the distribution with the machine-readable source of
|
||||
the software.
|
||||
|
||||
c) give non-standard binaries non-standard names, with
|
||||
instructions on where to get the original software distribution.
|
||||
|
||||
d) make other distribution arrangements with the author.
|
||||
|
||||
4. You may modify and include the part of the software into any other
|
||||
software (possibly commercial).
|
||||
|
||||
5. The scripts and library files supplied as input to or produced as
|
||||
output from the software do not automatically fall under the
|
||||
copyright of the software, but belong to whomever generated them,
|
||||
and may be sold commercially, and may be aggregated with this
|
||||
software.
|
||||
|
||||
6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE.
|
|
@ -0,0 +1,330 @@
|
|||
require 'bindata/base'
|
||||
require 'bindata/sanitize'
|
||||
|
||||
module BinData
|
||||
# An Array is a list of data objects of the same type.
|
||||
#
|
||||
# require 'bindata'
|
||||
#
|
||||
# data = "\x03\x04\x05\x06\x07\x08\x09"
|
||||
#
|
||||
# obj = BinData::Array.new(:type => :int8, :initial_length => 6)
|
||||
# obj.read(data)
|
||||
# obj.snapshot #=> [3, 4, 5, 6, 7, 8]
|
||||
#
|
||||
# obj = BinData::Array.new(:type => :int8,
|
||||
# :read_until => lambda { index == 1 })
|
||||
# obj.read(data)
|
||||
# obj.snapshot #=> [3, 4]
|
||||
#
|
||||
# obj = BinData::Array.new(:type => :int8,
|
||||
# :read_until => lambda { element >= 6 })
|
||||
# obj.read(data)
|
||||
# obj.snapshot #=> [3, 4, 5, 6]
|
||||
#
|
||||
# obj = BinData::Array.new(:type => :int8,
|
||||
# :read_until => lambda { array[index] + array[index - 1] == 13 })
|
||||
# obj.read(data)
|
||||
# obj.snapshot #=> [3, 4, 5, 6, 7]
|
||||
#
|
||||
# obj = BinData::Array.new(:type => :int8, :read_until => :eof)
|
||||
# obj.read(data)
|
||||
# obj.snapshot #=> [3, 4, 5, 6, 7, 8, 9]
|
||||
#
|
||||
# == Parameters
|
||||
#
|
||||
# Parameters may be provided at initialisation to control the behaviour of
|
||||
# an object. These params are:
|
||||
#
|
||||
# <tt>:type</tt>:: The symbol representing the data type of the
|
||||
# array elements. If the type is to have params
|
||||
# passed to it, then it should be provided as
|
||||
# <tt>[type_symbol, hash_params]</tt>.
|
||||
# <tt>:initial_length</tt>:: The initial length of the array.
|
||||
# <tt>:read_until</tt>:: While reading, elements are read until this
|
||||
# condition is true. This is typically used to
|
||||
# read an array until a sentinel value is found.
|
||||
# The variables +index+, +element+ and +array+
|
||||
# are made available to any lambda assigned to
|
||||
# this parameter. If the value of this parameter
|
||||
# is the symbol :eof, then the array will read
|
||||
# as much data from the stream as possible.
|
||||
#
|
||||
# Each data object in an array has the variable +index+ made available
|
||||
# to any lambda evaluated as a parameter of that data object.
|
||||
class Array < BinData::Base
|
||||
include Enumerable
|
||||
|
||||
# Register this class
|
||||
register(self.name, self)
|
||||
|
||||
# These are the parameters used by this class.
|
||||
mandatory_parameter :type
|
||||
optional_parameters :initial_length, :read_until
|
||||
mutually_exclusive_parameters :initial_length, :read_until
|
||||
|
||||
class << self
|
||||
# Ensures that +params+ is of the form expected by #initialize.
|
||||
def sanitize_parameters!(sanitizer, params)
|
||||
unless params.has_key?(:initial_length) or params.has_key?(:read_until)
|
||||
# ensure one of :initial_length and :read_until exists
|
||||
params[:initial_length] = 0
|
||||
end
|
||||
|
||||
if params.has_key?(:read_length)
|
||||
warn ":read_length is not used with arrays. You probably want to change this to :initial_length"
|
||||
end
|
||||
|
||||
if params.has_key?(:type)
|
||||
type, el_params = params[:type]
|
||||
params[:type] = sanitizer.sanitize(type, el_params)
|
||||
end
|
||||
|
||||
super(sanitizer, params)
|
||||
end
|
||||
end
|
||||
|
||||
# Creates a new Array
|
||||
def initialize(params = {}, env = nil)
|
||||
super(params, env)
|
||||
|
||||
klass, el_params = param(:type)
|
||||
|
||||
@element_list = nil
|
||||
@element_klass = klass
|
||||
@element_params = el_params
|
||||
end
|
||||
|
||||
# Returns if the element at position +index+ is clear?. If +index+
|
||||
# is not given, then returns whether all fields are clear.
|
||||
def clear?(index = nil)
|
||||
if @element_list.nil?
|
||||
true
|
||||
elsif index.nil?
|
||||
elements.each { |f| return false if not f.clear? }
|
||||
true
|
||||
else
|
||||
(index < elements.length) ? elements[index].clear? : true
|
||||
end
|
||||
end
|
||||
|
||||
# Clears the element at position +index+. If +index+ is not given, then
|
||||
# the internal state of the array is reset to that of a newly created
|
||||
# object.
|
||||
def clear(index = nil)
|
||||
if @element_list.nil?
|
||||
# do nothing as the array is already clear
|
||||
elsif index.nil?
|
||||
@element_list = nil
|
||||
elsif index < elements.length
|
||||
elements[index].clear
|
||||
end
|
||||
end
|
||||
|
||||
# Returns whether this data object contains a single value. Single
|
||||
# value data objects respond to <tt>#value</tt> and <tt>#value=</tt>.
|
||||
def single_value?
|
||||
return false
|
||||
end
|
||||
|
||||
# To be called after calling #do_read.
|
||||
def done_read
|
||||
elements.each { |f| f.done_read }
|
||||
end
|
||||
|
||||
# Appends a new element to the end of the array. If the array contains
|
||||
# single_values then the +value+ may be provided to the call.
|
||||
# Returns the appended object, or value in the case of single_values.
|
||||
def append(value = nil)
|
||||
# TODO: deprecate #append as it can be replaced with #push
|
||||
append_new_element
|
||||
self[-1] = value unless value.nil?
|
||||
self.last
|
||||
end
|
||||
|
||||
# Pushes the given object(s) on to the end of this array.
|
||||
# This expression returns the array itself, so several appends may
|
||||
# be chained together.
|
||||
def push(*args)
|
||||
args.each do |arg|
|
||||
if @element_klass == arg.class
|
||||
# TODO: need to modify arg.env to add_variable(:index) and
|
||||
# to link arg.env to self.env
|
||||
elements.push(arg)
|
||||
else
|
||||
append(arg)
|
||||
end
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
# Returns the element at +index+. If the element is a single_value
|
||||
# then the value of the element is returned instead.
|
||||
def [](*args)
|
||||
if args.length == 1 and ::Integer === args[0]
|
||||
# extend array automatically
|
||||
while args[0] >= elements.length
|
||||
append_new_element
|
||||
end
|
||||
end
|
||||
|
||||
data = elements[*args]
|
||||
if data.respond_to?(:each)
|
||||
data.collect { |el| (el && el.single_value?) ? el.value : el }
|
||||
else
|
||||
(data && data.single_value?) ? data.value : data
|
||||
end
|
||||
end
|
||||
alias_method :slice, :[]
|
||||
|
||||
# Sets the element at +index+. If the element is a single_value
|
||||
# then the value of the element is set instead.
|
||||
def []=(index, value)
|
||||
# extend array automatically
|
||||
while index >= elements.length
|
||||
append_new_element
|
||||
end
|
||||
|
||||
obj = elements[index]
|
||||
unless obj.single_value?
|
||||
# TODO: allow setting objects, not just values
|
||||
raise NoMethodError, "undefined method `[]=' for #{self}", caller
|
||||
end
|
||||
obj.value = value
|
||||
end
|
||||
|
||||
# Iterate over each element in the array. If the elements are
|
||||
# single_values then the values of the elements are iterated instead.
|
||||
def each
|
||||
elements.each do |el|
|
||||
yield(el.single_value? ? el.value : el)
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the first element, or the first +n+ elements, of the array.
|
||||
# If the array is empty, the first form returns nil, and the second
|
||||
# form returns an empty array.
|
||||
def first(n = nil)
|
||||
if n.nil?
|
||||
if elements.empty?
|
||||
# explicitly return nil as arrays grow automatically
|
||||
nil
|
||||
else
|
||||
self[0]
|
||||
end
|
||||
else
|
||||
self[0, n]
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the last element, or the last +n+ elements, of the array.
|
||||
# If the array is empty, the first form returns nil, and the second
|
||||
# form returns an empty array.
|
||||
def last(n = nil)
|
||||
if n.nil?
|
||||
self[-1]
|
||||
else
|
||||
n = length if n > length
|
||||
self[-n, n]
|
||||
end
|
||||
end
|
||||
|
||||
# The number of elements in this array.
|
||||
def length
|
||||
elements.length
|
||||
end
|
||||
alias_method :size, :length
|
||||
|
||||
# Returns true if self array contains no elements.
|
||||
def empty?
|
||||
length.zero?
|
||||
end
|
||||
|
||||
# Allow this object to be used in array context.
|
||||
def to_ary
|
||||
snapshot
|
||||
end
|
||||
|
||||
#---------------
|
||||
private
|
||||
|
||||
# Reads the values for all fields in this object from +io+.
|
||||
def _do_read(io)
|
||||
if has_param?(:initial_length)
|
||||
elements.each { |f| f.do_read(io) }
|
||||
elsif has_param?(:read_until)
|
||||
if param(:read_until) == :eof
|
||||
@element_list = nil
|
||||
loop do
|
||||
element = append_new_element
|
||||
begin
|
||||
element.do_read(io)
|
||||
rescue
|
||||
@element_list.pop
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
@element_list = nil
|
||||
loop do
|
||||
element = append_new_element
|
||||
element.do_read(io)
|
||||
variables = { :index => self.length - 1, :element => self.last,
|
||||
:array => self }
|
||||
finished = eval_param(:read_until, variables)
|
||||
break if finished
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Writes the values for all fields in this object to +io+.
|
||||
def _do_write(io)
|
||||
elements.each { |f| f.do_write(io) }
|
||||
end
|
||||
|
||||
# Returns the number of bytes it will take to write the element at
|
||||
# +index+. If +index+, then returns the number of bytes required
|
||||
# to write all fields.
|
||||
def _do_num_bytes(index)
|
||||
if index.nil?
|
||||
(elements.inject(0) { |sum, f| sum + f.do_num_bytes }).ceil
|
||||
else
|
||||
elements[index].do_num_bytes
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a snapshot of the data in this array.
|
||||
def _snapshot
|
||||
elements.collect { |e| e.snapshot }
|
||||
end
|
||||
|
||||
# Returns the list of all elements in the array. The elements
|
||||
# will be instantiated on the first call to this method.
|
||||
def elements
|
||||
if @element_list.nil?
|
||||
@element_list = []
|
||||
if has_param?(:initial_length)
|
||||
# create the desired number of instances
|
||||
eval_param(:initial_length).times do
|
||||
append_new_element
|
||||
end
|
||||
end
|
||||
end
|
||||
@element_list
|
||||
end
|
||||
|
||||
# Creates a new element and appends it to the end of @element_list.
|
||||
# Returns the newly created element
|
||||
def append_new_element
|
||||
# ensure @element_list is initialised
|
||||
elements()
|
||||
|
||||
env = create_env
|
||||
env.add_variable(:index, @element_list.length)
|
||||
element = @element_klass.new(@element_params, env)
|
||||
@element_list << element
|
||||
element
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,380 @@
|
|||
require 'bindata/io'
|
||||
require 'bindata/lazy'
|
||||
require 'bindata/registry'
|
||||
require 'bindata/sanitize'
|
||||
require 'stringio'
|
||||
|
||||
module BinData
|
||||
# Error raised when unexpected results occur when reading data from IO.
|
||||
class ValidityError < StandardError ; end
|
||||
|
||||
# This is the abstract base class for all data objects.
|
||||
#
|
||||
# == Parameters
|
||||
#
|
||||
# Parameters may be provided at initialisation to control the behaviour of
|
||||
# an object. These params are:
|
||||
#
|
||||
# [<tt>:readwrite</tt>] Deprecated. An alias for :onlyif.
|
||||
# [<tt>:onlyif</tt>] Used to indicate a data object is optional.
|
||||
# if false, calls to #read or #write will not
|
||||
# perform any I/O, #num_bytes will return 0 and
|
||||
# #snapshot will return nil. Default is true.
|
||||
# [<tt>:check_offset</tt>] Raise an error if the current IO offset doesn't
|
||||
# meet this criteria. A boolean return indicates
|
||||
# success or failure. Any other return is compared
|
||||
# to the current offset. The variable +offset+
|
||||
# is made available to any lambda assigned to
|
||||
# this parameter. This parameter is only checked
|
||||
# before reading.
|
||||
# [<tt>:adjust_offset</tt>] Ensures that the current IO offset is at this
|
||||
# position before reading. This is like
|
||||
# <tt>:check_offset</tt>, except that it will
|
||||
# adjust the IO offset instead of raising an error.
|
||||
class Base
|
||||
class << self
|
||||
# Returns the mandatory parameters used by this class. Any given args
|
||||
# are appended to the parameters list. The parameters for a class will
|
||||
# include the parameters of its ancestors.
|
||||
def mandatory_parameters(*args)
|
||||
unless defined? @mandatory_parameters
|
||||
@mandatory_parameters = []
|
||||
ancestors[1..-1].each do |parent|
|
||||
if parent.respond_to?(:mandatory_parameters)
|
||||
pmp = parent.mandatory_parameters
|
||||
@mandatory_parameters.concat(pmp)
|
||||
end
|
||||
end
|
||||
end
|
||||
if not args.empty?
|
||||
args.each { |arg| @mandatory_parameters << arg.to_sym }
|
||||
@mandatory_parameters.uniq!
|
||||
end
|
||||
@mandatory_parameters
|
||||
end
|
||||
alias_method :mandatory_parameter, :mandatory_parameters
|
||||
|
||||
# Returns the optional parameters used by this class. Any given args
|
||||
# are appended to the parameters list. The parameters for a class will
|
||||
# include the parameters of its ancestors.
|
||||
def optional_parameters(*args)
|
||||
unless defined? @optional_parameters
|
||||
@optional_parameters = []
|
||||
ancestors[1..-1].each do |parent|
|
||||
if parent.respond_to?(:optional_parameters)
|
||||
pop = parent.optional_parameters
|
||||
@optional_parameters.concat(pop)
|
||||
end
|
||||
end
|
||||
end
|
||||
if not args.empty?
|
||||
args.each { |arg| @optional_parameters << arg.to_sym }
|
||||
@optional_parameters.uniq!
|
||||
end
|
||||
@optional_parameters
|
||||
end
|
||||
alias_method :optional_parameter, :optional_parameters
|
||||
|
||||
# Returns the default parameters used by this class. Any given args
|
||||
# are appended to the parameters list. The parameters for a class will
|
||||
# include the parameters of its ancestors.
|
||||
def default_parameters(params = {})
|
||||
unless defined? @default_parameters
|
||||
@default_parameters = {}
|
||||
ancestors[1..-1].each do |parent|
|
||||
if parent.respond_to?(:default_parameters)
|
||||
pdp = parent.default_parameters
|
||||
@default_parameters = @default_parameters.merge(pdp)
|
||||
end
|
||||
end
|
||||
end
|
||||
if not params.empty?
|
||||
@default_parameters = @default_parameters.merge(params)
|
||||
end
|
||||
@default_parameters
|
||||
end
|
||||
alias_method :default_parameter, :default_parameters
|
||||
|
||||
# Returns the pairs of mutually exclusive parameters used by this class.
|
||||
# Any given args are appended to the parameters list. The parameters for
|
||||
# a class will include the parameters of its ancestors.
|
||||
def mutually_exclusive_parameters(*args)
|
||||
unless defined? @mutually_exclusive_parameters
|
||||
@mutually_exclusive_parameters = []
|
||||
ancestors[1..-1].each do |parent|
|
||||
if parent.respond_to?(:mutually_exclusive_parameters)
|
||||
pmep = parent.mutually_exclusive_parameters
|
||||
@mutually_exclusive_parameters.concat(pmep)
|
||||
end
|
||||
end
|
||||
end
|
||||
if not args.empty?
|
||||
@mutually_exclusive_parameters << [args[0].to_sym, args[1].to_sym]
|
||||
end
|
||||
@mutually_exclusive_parameters
|
||||
end
|
||||
|
||||
# Returns a list of parameters that are accepted by this object
|
||||
def accepted_parameters
|
||||
(mandatory_parameters + optional_parameters + default_parameters.keys).uniq
|
||||
end
|
||||
|
||||
# Ensures that +params+ is of the form expected by #initialize.
|
||||
def sanitize_parameters!(sanitizer, params)
|
||||
# replace :readwrite with :onlyif
|
||||
if params.has_key?(:readwrite)
|
||||
warn ":readwrite is deprecated. Replacing with :onlyif"
|
||||
params[:onlyif] = params.delete(:readwrite)
|
||||
end
|
||||
|
||||
# add default parameters
|
||||
default_parameters.each do |k,v|
|
||||
params[k] = v unless params.has_key?(k)
|
||||
end
|
||||
|
||||
# ensure mandatory parameters exist
|
||||
mandatory_parameters.each do |prm|
|
||||
if not params.has_key?(prm)
|
||||
raise ArgumentError, "parameter ':#{prm}' must be specified " +
|
||||
"in #{self}"
|
||||
end
|
||||
end
|
||||
|
||||
# ensure mutual exclusion
|
||||
mutually_exclusive_parameters.each do |param1, param2|
|
||||
if params.has_key?(param1) and params.has_key?(param2)
|
||||
raise ArgumentError, "params #{param1} and #{param2} " +
|
||||
"are mutually exclusive"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Instantiates this class and reads from +io+. For single value objects
|
||||
# just the value is returned, otherwise the newly created data object is
|
||||
# returned.
|
||||
def read(io)
|
||||
data = self.new
|
||||
data.read(io)
|
||||
data.single_value? ? data.value : data
|
||||
end
|
||||
|
||||
# Registers the mapping of +name+ to +klass+.
|
||||
def register(name, klass)
|
||||
Registry.instance.register(name, klass)
|
||||
end
|
||||
private :register
|
||||
end
|
||||
|
||||
# Define the parameters we use in this class.
|
||||
optional_parameters :check_offset, :adjust_offset
|
||||
default_parameters :onlyif => true
|
||||
mutually_exclusive_parameters :check_offset, :adjust_offset
|
||||
|
||||
# Creates a new data object.
|
||||
#
|
||||
# +params+ is a hash containing symbol keys. Some params may
|
||||
# reference callable objects (methods or procs). +env+ is the
|
||||
# environment that these callable objects are evaluated in.
|
||||
def initialize(params = {}, env = nil)
|
||||
unless SanitizedParameters === params
|
||||
params = Sanitizer.sanitize(self, params)
|
||||
end
|
||||
|
||||
@params = params.accepted_parameters
|
||||
|
||||
# set up the environment
|
||||
@env = env || LazyEvalEnv.new
|
||||
@env.params = params.extra_parameters
|
||||
@env.data_object = self
|
||||
end
|
||||
|
||||
# Reads data into this data object by calling #do_read then #done_read.
|
||||
def read(io)
|
||||
io = BinData::IO.new(io) unless BinData::IO === io
|
||||
|
||||
do_read(io)
|
||||
done_read
|
||||
self
|
||||
end
|
||||
|
||||
# Reads the value for this data from +io+.
|
||||
def do_read(io)
|
||||
raise ArgumentError, "io must be a BinData::IO" unless BinData::IO === io
|
||||
|
||||
clear
|
||||
check_offset(io)
|
||||
|
||||
if eval_param(:onlyif)
|
||||
_do_read(io)
|
||||
end
|
||||
end
|
||||
|
||||
# Writes the value for this data to +io+ by calling #do_write.
|
||||
def write(io)
|
||||
io = BinData::IO.new(io) unless BinData::IO === io
|
||||
|
||||
do_write(io)
|
||||
io.flush
|
||||
self
|
||||
end
|
||||
|
||||
|
||||
# Writes the value for this data to +io+.
|
||||
def do_write(io)
|
||||
raise ArgumentError, "io must be a BinData::IO" unless BinData::IO === io
|
||||
|
||||
if eval_param(:onlyif)
|
||||
_do_write(io)
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the number of bytes it will take to write this data by calling
|
||||
# #do_num_bytes.
|
||||
def num_bytes(what = nil)
|
||||
num = do_num_bytes(what)
|
||||
num.ceil
|
||||
end
|
||||
|
||||
# Returns the number of bytes it will take to write this data.
|
||||
def do_num_bytes(what = nil)
|
||||
if eval_param(:onlyif)
|
||||
_do_num_bytes(what)
|
||||
else
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a snapshot of this data object.
|
||||
# Returns nil if :onlyif is false
|
||||
def snapshot
|
||||
if eval_param(:onlyif)
|
||||
_snapshot
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the string representation of this data object.
|
||||
def to_s
|
||||
io = StringIO.new
|
||||
write(io)
|
||||
io.rewind
|
||||
io.read
|
||||
end
|
||||
|
||||
# Return a human readable representation of this object.
|
||||
def inspect
|
||||
snapshot.inspect
|
||||
end
|
||||
|
||||
#---------------
|
||||
private
|
||||
|
||||
# Creates a new LazyEvalEnv for use by a child data object.
|
||||
def create_env
|
||||
LazyEvalEnv.new(@env)
|
||||
end
|
||||
|
||||
# Returns the value of the evaluated parameter. +key+ references a
|
||||
# parameter from the +params+ hash used when creating the data object.
|
||||
# +values+ contains data that may be accessed when evaluating +key+.
|
||||
# Returns nil if +key+ does not refer to any parameter.
|
||||
def eval_param(key, values = nil)
|
||||
@env.lazy_eval(@params[key], values)
|
||||
end
|
||||
|
||||
# Returns the parameter from the +params+ hash referenced by +key+.
|
||||
# Use this method if you are sure the parameter is not to be evaluated.
|
||||
# You most likely want #eval_param.
|
||||
def param(key)
|
||||
@params[key]
|
||||
end
|
||||
|
||||
# Returns whether +key+ exists in the +params+ hash used when creating
|
||||
# this data object.
|
||||
def has_param?(key)
|
||||
@params.has_key?(key.to_sym)
|
||||
end
|
||||
|
||||
# Checks that the current offset of +io+ is as expected. This should
|
||||
# be called from #do_read before performing the reading.
|
||||
def check_offset(io)
|
||||
if has_param?(:check_offset)
|
||||
actual_offset = io.offset
|
||||
expected = eval_param(:check_offset, :offset => actual_offset)
|
||||
|
||||
if not expected
|
||||
raise ValidityError, "offset not as expected"
|
||||
elsif actual_offset != expected and expected != true
|
||||
raise ValidityError, "offset is '#{actual_offset}' but " +
|
||||
"expected '#{expected}'"
|
||||
end
|
||||
elsif has_param?(:adjust_offset)
|
||||
actual_offset = io.offset
|
||||
expected = eval_param(:adjust_offset)
|
||||
if actual_offset != expected
|
||||
begin
|
||||
seek = expected - actual_offset
|
||||
io.seekbytes(seek)
|
||||
warn "adjusting stream position by #{seek} bytes" if $VERBOSE
|
||||
rescue
|
||||
# could not seek so raise an error
|
||||
raise ValidityError, "offset is '#{actual_offset}' but " +
|
||||
"couldn't seek to expected '#{expected}'"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
###########################################################################
|
||||
# To be implemented by subclasses
|
||||
|
||||
# Resets the internal state to that of a newly created object.
|
||||
def clear
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Returns true if the object has not been changed since creation.
|
||||
def clear?(*args)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Returns whether this data object contains a single value. Single
|
||||
# value data objects respond to <tt>#value</tt> and <tt>#value=</tt>.
|
||||
def single_value?
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# To be called after calling #do_read.
|
||||
def done_read
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Reads the data for this data object from +io+.
|
||||
def _do_read(io)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Writes the value for this data to +io+.
|
||||
def _do_write(io)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Returns the number of bytes it will take to write this data.
|
||||
def _do_num_bytes
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Returns a snapshot of this data object.
|
||||
def _snapshot
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Set visibility requirements of methods to implement
|
||||
public :clear, :clear?, :single_value?, :done_read
|
||||
private :_do_read, :_do_write, :_do_num_bytes, :_snapshot
|
||||
|
||||
# End To be implemented by subclasses
|
||||
###########################################################################
|
||||
end
|
||||
end
|
|
@ -0,0 +1,807 @@
|
|||
require 'bindata/single'
|
||||
|
||||
module BinData
|
||||
# Provides a number of classes that contain an integer. The integer
|
||||
# is defined by endian, signedness and number of bytes.
|
||||
|
||||
module BitField #:nodoc: all
|
||||
def self.create_methods(klass, nbits, endian)
|
||||
min = 0
|
||||
max = (1 << nbits) - 1
|
||||
clamp = "val = (val < #{min}) ? #{min} : (val > #{max}) ? #{max} : val"
|
||||
|
||||
# allow single bits to be used as booleans
|
||||
if nbits == 1
|
||||
clamp = "val = (val == true) ? 1 : (not val) ? 0 : #{clamp}"
|
||||
end
|
||||
|
||||
define_methods(klass, nbits, endian.inspect, clamp)
|
||||
end
|
||||
|
||||
def self.define_methods(klass, nbits, endian, clamp)
|
||||
# define methods in the given class
|
||||
klass.module_eval <<-END
|
||||
def value=(val)
|
||||
#{clamp}
|
||||
super(val)
|
||||
end
|
||||
|
||||
#---------------
|
||||
private
|
||||
|
||||
def _do_write(io)
|
||||
raise "can't write whilst reading" if @in_read
|
||||
io.writebits(_value, #{nbits}, #{endian})
|
||||
end
|
||||
|
||||
def _do_num_bytes(ignored)
|
||||
#{nbits} / 8.0
|
||||
end
|
||||
|
||||
def read_val(io)
|
||||
io.readbits(#{nbits}, #{endian})
|
||||
end
|
||||
|
||||
def sensible_default
|
||||
0
|
||||
end
|
||||
END
|
||||
end
|
||||
end
|
||||
|
||||
# 1 bit big endian bitfield.
|
||||
class Bit1 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 1, :big)
|
||||
end
|
||||
|
||||
# 1 bit little endian bitfield.
|
||||
class Bit1le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 1, :little)
|
||||
end
|
||||
|
||||
# 2 bit big endian bitfield.
|
||||
class Bit2 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 2, :big)
|
||||
end
|
||||
|
||||
# 2 bit little endian bitfield.
|
||||
class Bit2le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 2, :little)
|
||||
end
|
||||
|
||||
# 3 bit big endian bitfield.
|
||||
class Bit3 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 3, :big)
|
||||
end
|
||||
|
||||
# 3 bit little endian bitfield.
|
||||
class Bit3le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 3, :little)
|
||||
end
|
||||
|
||||
# 4 bit big endian bitfield.
|
||||
class Bit4 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 4, :big)
|
||||
end
|
||||
|
||||
# 4 bit little endian bitfield.
|
||||
class Bit4le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 4, :little)
|
||||
end
|
||||
|
||||
# 5 bit big endian bitfield.
|
||||
class Bit5 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 5, :big)
|
||||
end
|
||||
|
||||
# 5 bit little endian bitfield.
|
||||
class Bit5le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 5, :little)
|
||||
end
|
||||
|
||||
# 6 bit big endian bitfield.
|
||||
class Bit6 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 6, :big)
|
||||
end
|
||||
|
||||
# 6 bit little endian bitfield.
|
||||
class Bit6le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 6, :little)
|
||||
end
|
||||
|
||||
# 7 bit big endian bitfield.
|
||||
class Bit7 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 7, :big)
|
||||
end
|
||||
|
||||
# 7 bit little endian bitfield.
|
||||
class Bit7le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 7, :little)
|
||||
end
|
||||
|
||||
# 8 bit big endian bitfield.
|
||||
class Bit8 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 8, :big)
|
||||
end
|
||||
|
||||
# 8 bit little endian bitfield.
|
||||
class Bit8le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 8, :little)
|
||||
end
|
||||
|
||||
# 9 bit big endian bitfield.
|
||||
class Bit9 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 9, :big)
|
||||
end
|
||||
|
||||
# 9 bit little endian bitfield.
|
||||
class Bit9le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 9, :little)
|
||||
end
|
||||
|
||||
# 10 bit big endian bitfield.
|
||||
class Bit10 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 10, :big)
|
||||
end
|
||||
|
||||
# 10 bit little endian bitfield.
|
||||
class Bit10le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 10, :little)
|
||||
end
|
||||
|
||||
# 11 bit big endian bitfield.
|
||||
class Bit11 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 11, :big)
|
||||
end
|
||||
|
||||
# 11 bit little endian bitfield.
|
||||
class Bit11le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 11, :little)
|
||||
end
|
||||
|
||||
# 12 bit big endian bitfield.
|
||||
class Bit12 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 12, :big)
|
||||
end
|
||||
|
||||
# 12 bit little endian bitfield.
|
||||
class Bit12le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 12, :little)
|
||||
end
|
||||
|
||||
# 13 bit big endian bitfield.
|
||||
class Bit13 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 13, :big)
|
||||
end
|
||||
|
||||
# 13 bit little endian bitfield.
|
||||
class Bit13le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 13, :little)
|
||||
end
|
||||
|
||||
# 14 bit big endian bitfield.
|
||||
class Bit14 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 14, :big)
|
||||
end
|
||||
|
||||
# 14 bit little endian bitfield.
|
||||
class Bit14le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 14, :little)
|
||||
end
|
||||
|
||||
# 15 bit big endian bitfield.
|
||||
class Bit15 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 15, :big)
|
||||
end
|
||||
|
||||
# 15 bit little endian bitfield.
|
||||
class Bit15le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 15, :little)
|
||||
end
|
||||
|
||||
# 16 bit big endian bitfield.
|
||||
class Bit16 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 16, :big)
|
||||
end
|
||||
|
||||
# 16 bit little endian bitfield.
|
||||
class Bit16le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 16, :little)
|
||||
end
|
||||
|
||||
# 17 bit big endian bitfield.
|
||||
class Bit17 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 17, :big)
|
||||
end
|
||||
|
||||
# 17 bit little endian bitfield.
|
||||
class Bit17le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 17, :little)
|
||||
end
|
||||
|
||||
# 18 bit big endian bitfield.
|
||||
class Bit18 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 18, :big)
|
||||
end
|
||||
|
||||
# 18 bit little endian bitfield.
|
||||
class Bit18le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 18, :little)
|
||||
end
|
||||
|
||||
# 19 bit big endian bitfield.
|
||||
class Bit19 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 19, :big)
|
||||
end
|
||||
|
||||
# 19 bit little endian bitfield.
|
||||
class Bit19le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 19, :little)
|
||||
end
|
||||
|
||||
# 20 bit big endian bitfield.
|
||||
class Bit20 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 20, :big)
|
||||
end
|
||||
|
||||
# 20 bit little endian bitfield.
|
||||
class Bit20le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 20, :little)
|
||||
end
|
||||
|
||||
# 21 bit big endian bitfield.
|
||||
class Bit21 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 21, :big)
|
||||
end
|
||||
|
||||
# 21 bit little endian bitfield.
|
||||
class Bit21le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 21, :little)
|
||||
end
|
||||
|
||||
# 22 bit big endian bitfield.
|
||||
class Bit22 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 22, :big)
|
||||
end
|
||||
|
||||
# 22 bit little endian bitfield.
|
||||
class Bit22le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 22, :little)
|
||||
end
|
||||
|
||||
# 23 bit big endian bitfield.
|
||||
class Bit23 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 23, :big)
|
||||
end
|
||||
|
||||
# 23 bit little endian bitfield.
|
||||
class Bit23le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 23, :little)
|
||||
end
|
||||
|
||||
# 24 bit big endian bitfield.
|
||||
class Bit24 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 24, :big)
|
||||
end
|
||||
|
||||
# 24 bit little endian bitfield.
|
||||
class Bit24le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 24, :little)
|
||||
end
|
||||
|
||||
# 25 bit big endian bitfield.
|
||||
class Bit25 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 25, :big)
|
||||
end
|
||||
|
||||
# 25 bit little endian bitfield.
|
||||
class Bit25le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 25, :little)
|
||||
end
|
||||
|
||||
# 26 bit big endian bitfield.
|
||||
class Bit26 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 26, :big)
|
||||
end
|
||||
|
||||
# 26 bit little endian bitfield.
|
||||
class Bit26le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 26, :little)
|
||||
end
|
||||
|
||||
# 27 bit big endian bitfield.
|
||||
class Bit27 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 27, :big)
|
||||
end
|
||||
|
||||
# 27 bit little endian bitfield.
|
||||
class Bit27le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 27, :little)
|
||||
end
|
||||
|
||||
# 28 bit big endian bitfield.
|
||||
class Bit28 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 28, :big)
|
||||
end
|
||||
|
||||
# 28 bit little endian bitfield.
|
||||
class Bit28le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 28, :little)
|
||||
end
|
||||
|
||||
# 29 bit big endian bitfield.
|
||||
class Bit29 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 29, :big)
|
||||
end
|
||||
|
||||
# 29 bit little endian bitfield.
|
||||
class Bit29le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 29, :little)
|
||||
end
|
||||
|
||||
# 30 bit big endian bitfield.
|
||||
class Bit30 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 30, :big)
|
||||
end
|
||||
|
||||
# 30 bit little endian bitfield.
|
||||
class Bit30le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 30, :little)
|
||||
end
|
||||
|
||||
# 31 bit big endian bitfield.
|
||||
class Bit31 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 31, :big)
|
||||
end
|
||||
|
||||
# 31 bit little endian bitfield.
|
||||
class Bit31le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 31, :little)
|
||||
end
|
||||
|
||||
# 32 bit big endian bitfield.
|
||||
class Bit32 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 32, :big)
|
||||
end
|
||||
|
||||
# 32 bit little endian bitfield.
|
||||
class Bit32le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 32, :little)
|
||||
end
|
||||
|
||||
# 33 bit big endian bitfield.
|
||||
class Bit33 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 33, :big)
|
||||
end
|
||||
|
||||
# 33 bit little endian bitfield.
|
||||
class Bit33le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 33, :little)
|
||||
end
|
||||
|
||||
# 34 bit big endian bitfield.
|
||||
class Bit34 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 34, :big)
|
||||
end
|
||||
|
||||
# 34 bit little endian bitfield.
|
||||
class Bit34le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 34, :little)
|
||||
end
|
||||
|
||||
# 35 bit big endian bitfield.
|
||||
class Bit35 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 35, :big)
|
||||
end
|
||||
|
||||
# 35 bit little endian bitfield.
|
||||
class Bit35le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 35, :little)
|
||||
end
|
||||
|
||||
# 36 bit big endian bitfield.
|
||||
class Bit36 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 36, :big)
|
||||
end
|
||||
|
||||
# 36 bit little endian bitfield.
|
||||
class Bit36le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 36, :little)
|
||||
end
|
||||
|
||||
# 37 bit big endian bitfield.
|
||||
class Bit37 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 37, :big)
|
||||
end
|
||||
|
||||
# 37 bit little endian bitfield.
|
||||
class Bit37le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 37, :little)
|
||||
end
|
||||
|
||||
# 38 bit big endian bitfield.
|
||||
class Bit38 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 38, :big)
|
||||
end
|
||||
|
||||
# 38 bit little endian bitfield.
|
||||
class Bit38le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 38, :little)
|
||||
end
|
||||
|
||||
# 39 bit big endian bitfield.
|
||||
class Bit39 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 39, :big)
|
||||
end
|
||||
|
||||
# 39 bit little endian bitfield.
|
||||
class Bit39le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 39, :little)
|
||||
end
|
||||
|
||||
# 40 bit big endian bitfield.
|
||||
class Bit40 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 40, :big)
|
||||
end
|
||||
|
||||
# 40 bit little endian bitfield.
|
||||
class Bit40le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 40, :little)
|
||||
end
|
||||
|
||||
# 41 bit big endian bitfield.
|
||||
class Bit41 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 41, :big)
|
||||
end
|
||||
|
||||
# 41 bit little endian bitfield.
|
||||
class Bit41le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 41, :little)
|
||||
end
|
||||
|
||||
# 42 bit big endian bitfield.
|
||||
class Bit42 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 42, :big)
|
||||
end
|
||||
|
||||
# 42 bit little endian bitfield.
|
||||
class Bit42le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 42, :little)
|
||||
end
|
||||
|
||||
# 43 bit big endian bitfield.
|
||||
class Bit43 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 43, :big)
|
||||
end
|
||||
|
||||
# 43 bit little endian bitfield.
|
||||
class Bit43le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 43, :little)
|
||||
end
|
||||
|
||||
# 44 bit big endian bitfield.
|
||||
class Bit44 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 44, :big)
|
||||
end
|
||||
|
||||
# 44 bit little endian bitfield.
|
||||
class Bit44le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 44, :little)
|
||||
end
|
||||
|
||||
# 45 bit big endian bitfield.
|
||||
class Bit45 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 45, :big)
|
||||
end
|
||||
|
||||
# 45 bit little endian bitfield.
|
||||
class Bit45le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 45, :little)
|
||||
end
|
||||
|
||||
# 46 bit big endian bitfield.
|
||||
class Bit46 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 46, :big)
|
||||
end
|
||||
|
||||
# 46 bit little endian bitfield.
|
||||
class Bit46le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 46, :little)
|
||||
end
|
||||
|
||||
# 47 bit big endian bitfield.
|
||||
class Bit47 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 47, :big)
|
||||
end
|
||||
|
||||
# 47 bit little endian bitfield.
|
||||
class Bit47le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 47, :little)
|
||||
end
|
||||
|
||||
# 48 bit big endian bitfield.
|
||||
class Bit48 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 48, :big)
|
||||
end
|
||||
|
||||
# 48 bit little endian bitfield.
|
||||
class Bit48le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 48, :little)
|
||||
end
|
||||
|
||||
# 49 bit big endian bitfield.
|
||||
class Bit49 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 49, :big)
|
||||
end
|
||||
|
||||
# 49 bit little endian bitfield.
|
||||
class Bit49le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 49, :little)
|
||||
end
|
||||
|
||||
# 50 bit big endian bitfield.
|
||||
class Bit50 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 50, :big)
|
||||
end
|
||||
|
||||
# 50 bit little endian bitfield.
|
||||
class Bit50le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 50, :little)
|
||||
end
|
||||
|
||||
# 51 bit big endian bitfield.
|
||||
class Bit51 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 51, :big)
|
||||
end
|
||||
|
||||
# 51 bit little endian bitfield.
|
||||
class Bit51le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 51, :little)
|
||||
end
|
||||
|
||||
# 52 bit big endian bitfield.
|
||||
class Bit52 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 52, :big)
|
||||
end
|
||||
|
||||
# 52 bit little endian bitfield.
|
||||
class Bit52le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 52, :little)
|
||||
end
|
||||
|
||||
# 53 bit big endian bitfield.
|
||||
class Bit53 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 53, :big)
|
||||
end
|
||||
|
||||
# 53 bit little endian bitfield.
|
||||
class Bit53le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 53, :little)
|
||||
end
|
||||
|
||||
# 54 bit big endian bitfield.
|
||||
class Bit54 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 54, :big)
|
||||
end
|
||||
|
||||
# 54 bit little endian bitfield.
|
||||
class Bit54le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 54, :little)
|
||||
end
|
||||
|
||||
# 55 bit big endian bitfield.
|
||||
class Bit55 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 55, :big)
|
||||
end
|
||||
|
||||
# 55 bit little endian bitfield.
|
||||
class Bit55le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 55, :little)
|
||||
end
|
||||
|
||||
# 56 bit big endian bitfield.
|
||||
class Bit56 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 56, :big)
|
||||
end
|
||||
|
||||
# 56 bit little endian bitfield.
|
||||
class Bit56le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 56, :little)
|
||||
end
|
||||
|
||||
# 57 bit big endian bitfield.
|
||||
class Bit57 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 57, :big)
|
||||
end
|
||||
|
||||
# 57 bit little endian bitfield.
|
||||
class Bit57le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 57, :little)
|
||||
end
|
||||
|
||||
# 58 bit big endian bitfield.
|
||||
class Bit58 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 58, :big)
|
||||
end
|
||||
|
||||
# 58 bit little endian bitfield.
|
||||
class Bit58le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 58, :little)
|
||||
end
|
||||
|
||||
# 59 bit big endian bitfield.
|
||||
class Bit59 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 59, :big)
|
||||
end
|
||||
|
||||
# 59 bit little endian bitfield.
|
||||
class Bit59le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 59, :little)
|
||||
end
|
||||
|
||||
# 60 bit big endian bitfield.
|
||||
class Bit60 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 60, :big)
|
||||
end
|
||||
|
||||
# 60 bit little endian bitfield.
|
||||
class Bit60le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 60, :little)
|
||||
end
|
||||
|
||||
# 61 bit big endian bitfield.
|
||||
class Bit61 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 61, :big)
|
||||
end
|
||||
|
||||
# 61 bit little endian bitfield.
|
||||
class Bit61le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 61, :little)
|
||||
end
|
||||
|
||||
# 62 bit big endian bitfield.
|
||||
class Bit62 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 62, :big)
|
||||
end
|
||||
|
||||
# 62 bit little endian bitfield.
|
||||
class Bit62le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 62, :little)
|
||||
end
|
||||
|
||||
# 63 bit big endian bitfield.
|
||||
class Bit63 < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 63, :big)
|
||||
end
|
||||
|
||||
# 63 bit little endian bitfield.
|
||||
class Bit63le < BinData::Single
|
||||
register(self.name, self)
|
||||
BitField.create_methods(self, 63, :little)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,163 @@
|
|||
require 'forwardable'
|
||||
require 'bindata/base'
|
||||
require 'bindata/sanitize'
|
||||
|
||||
module BinData
|
||||
# A Choice is a collection of data objects of which only one is active
|
||||
# at any particular time.
|
||||
#
|
||||
# require 'bindata'
|
||||
#
|
||||
# type1 = [:string, {:value => "Type1"}]
|
||||
# type2 = [:string, {:value => "Type2"}]
|
||||
#
|
||||
# choices = [ type1, type2 ]
|
||||
# a = BinData::Choice.new(:choices => choices, :selection => 1)
|
||||
# a.value # => "Type2"
|
||||
#
|
||||
# choices = [ nil, nil, nil, type1, nil, type2 ]
|
||||
# a = BinData::Choice.new(:choices => choices, :selection => 3)
|
||||
# a.value # => "Type1"
|
||||
#
|
||||
# choices = {5 => type1, 17 => type2}
|
||||
# a = BinData::Choice.new(:choices => choices, :selection => 5)
|
||||
# a.value # => "Type1"
|
||||
#
|
||||
# mychoice = 'big'
|
||||
# choices = {'big' => :uint16be, 'little' => :uint16le}
|
||||
# a = BinData::Choice.new(:choices => choices,
|
||||
# :selection => lambda { mychoice })
|
||||
# a.value = 256
|
||||
# a.to_s #=> "\001\000"
|
||||
# mychoice[0..-1] = 'little'
|
||||
# a.to_s #=> "\000\001"
|
||||
#
|
||||
#
|
||||
# == Parameters
|
||||
#
|
||||
# Parameters may be provided at initialisation to control the behaviour of
|
||||
# an object. These params are:
|
||||
#
|
||||
# <tt>:choices</tt>:: Either an array or a hash specifying the possible
|
||||
# data objects. The format of the array/hash.values is
|
||||
# a list of symbols representing the data object type.
|
||||
# If a choice is to have params passed to it, then it
|
||||
# should be provided as [type_symbol, hash_params].
|
||||
# An implementation gotcha is that the hash may not
|
||||
# contain symbols as keys.
|
||||
# <tt>:selection</tt>:: An index/key into the :choices array/hash which
|
||||
# specifies the currently active choice.
|
||||
class Choice < BinData::Base
|
||||
extend Forwardable
|
||||
|
||||
# Register this class
|
||||
register(self.name, self)
|
||||
|
||||
# These are the parameters used by this class.
|
||||
mandatory_parameters :choices, :selection
|
||||
|
||||
class << self
|
||||
|
||||
# Ensures that +params+ is of the form expected by #initialize.
|
||||
def sanitize_parameters!(sanitizer, params)
|
||||
if params.has_key?(:choices)
|
||||
choices = params[:choices]
|
||||
|
||||
case choices
|
||||
when ::Hash
|
||||
new_choices = {}
|
||||
choices.keys.each do |key|
|
||||
# ensure valid hash keys
|
||||
if Symbol === key
|
||||
msg = ":choices hash may not have symbols for keys"
|
||||
raise ArgumentError, msg
|
||||
elsif key.nil?
|
||||
raise ArgumentError, ":choices hash may not have nil key"
|
||||
end
|
||||
|
||||
# collect sanitized choice values
|
||||
type, param = choices[key]
|
||||
new_choices[key] = sanitizer.sanitize(type, param)
|
||||
end
|
||||
params[:choices] = new_choices
|
||||
when ::Array
|
||||
choices.collect! do |type, param|
|
||||
if type.nil?
|
||||
# allow sparse arrays
|
||||
nil
|
||||
else
|
||||
sanitizer.sanitize(type, param)
|
||||
end
|
||||
end
|
||||
params[:choices] = choices
|
||||
else
|
||||
raise ArgumentError, "unknown type for :choices (#{choices.class})"
|
||||
end
|
||||
end
|
||||
|
||||
super(sanitizer, params)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(params = {}, env = nil)
|
||||
super(params, env)
|
||||
|
||||
# prepare collection of instantiated choice objects
|
||||
@choices = (param(:choices) === ::Array) ? [] : {}
|
||||
@last_key = nil
|
||||
end
|
||||
|
||||
def_delegators :the_choice, :clear, :clear?, :single_value?
|
||||
def_delegators :the_choice, :done_read, :_snapshot
|
||||
def_delegators :the_choice, :_do_read, :_do_write, :_do_num_bytes
|
||||
|
||||
# Override to include selected data object.
|
||||
def respond_to?(symbol, include_private = false)
|
||||
super || the_choice.respond_to?(symbol, include_private)
|
||||
end
|
||||
|
||||
def method_missing(symbol, *args, &block)
|
||||
if the_choice.respond_to?(symbol)
|
||||
the_choice.__send__(symbol, *args, &block)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
#---------------
|
||||
private
|
||||
|
||||
# Returns the selected data object.
|
||||
def the_choice
|
||||
key = eval_param(:selection)
|
||||
|
||||
if key.nil?
|
||||
raise IndexError, ":selection returned nil value"
|
||||
end
|
||||
|
||||
obj = @choices[key]
|
||||
if obj.nil?
|
||||
# instantiate choice object
|
||||
choice_klass, choice_params = param(:choices)[key]
|
||||
if choice_klass.nil?
|
||||
raise IndexError, "selection #{key} does not exist in :choices"
|
||||
end
|
||||
obj = choice_klass.new(choice_params, create_env)
|
||||
@choices[key] = obj
|
||||
end
|
||||
|
||||
# for single_values copy the value when the selected object changes
|
||||
if key != @last_key
|
||||
if @last_key != nil
|
||||
prev = @choices[@last_key]
|
||||
if prev != nil and prev.single_value? and obj.single_value?
|
||||
obj.value = prev.value
|
||||
end
|
||||
end
|
||||
@last_key = key
|
||||
end
|
||||
|
||||
obj
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,88 @@
|
|||
require 'bindata/single'
|
||||
|
||||
module BinData
|
||||
# Provides a number of classes that contain a floating point number.
|
||||
# The float is defined by endian, and precision.
|
||||
|
||||
module Float #:nodoc: all
|
||||
def self.create_float_methods(klass, single_precision, endian)
|
||||
read = create_read_code(single_precision, endian)
|
||||
to_s = create_to_s_code(single_precision, endian)
|
||||
|
||||
define_methods(klass, single_precision, read, to_s)
|
||||
end
|
||||
|
||||
def self.create_read_code(single_precision, endian)
|
||||
if single_precision
|
||||
unpack = (endian == :little) ? 'e' : 'g'
|
||||
nbytes = 4
|
||||
else # double_precision
|
||||
unpack = (endian == :little) ? 'E' : 'G'
|
||||
nbytes = 8
|
||||
end
|
||||
|
||||
"io.readbytes(#{nbytes}).unpack('#{unpack}').at(0)"
|
||||
end
|
||||
|
||||
def self.create_to_s_code(single_precision, endian)
|
||||
if single_precision
|
||||
pack = (endian == :little) ? 'e' : 'g'
|
||||
else # double_precision
|
||||
pack = (endian == :little) ? 'E' : 'G'
|
||||
end
|
||||
|
||||
"[val].pack('#{pack}')"
|
||||
end
|
||||
|
||||
def self.define_methods(klass, single_precision, read, to_s)
|
||||
nbytes = single_precision ? 4 : 8
|
||||
|
||||
# define methods in the given class
|
||||
klass.module_eval <<-END
|
||||
def _do_num_bytes(ignored)
|
||||
#{nbytes}
|
||||
end
|
||||
|
||||
#---------------
|
||||
private
|
||||
|
||||
def sensible_default
|
||||
0.0
|
||||
end
|
||||
|
||||
def val_to_str(val)
|
||||
#{to_s}
|
||||
end
|
||||
|
||||
def read_val(io)
|
||||
#{read}
|
||||
end
|
||||
END
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Single precision floating point number in little endian format
|
||||
class FloatLe < BinData::Single
|
||||
register(self.name, self)
|
||||
Float.create_float_methods(self, true, :little)
|
||||
end
|
||||
|
||||
# Single precision floating point number in big endian format
|
||||
class FloatBe < BinData::Single
|
||||
register(self.name, self)
|
||||
Float.create_float_methods(self, true, :big)
|
||||
end
|
||||
|
||||
# Double precision floating point number in little endian format
|
||||
class DoubleLe < BinData::Single
|
||||
register(self.name, self)
|
||||
Float.create_float_methods(self, false, :little)
|
||||
end
|
||||
|
||||
# Double precision floating point number in big endian format
|
||||
class DoubleBe < BinData::Single
|
||||
register(self.name, self)
|
||||
Float.create_float_methods(self, false, :big)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,185 @@
|
|||
require 'bindata/single'
|
||||
|
||||
module BinData
|
||||
# Provides a number of classes that contain an integer. The integer
|
||||
# is defined by endian, signedness and number of bytes.
|
||||
|
||||
module Integer #:nodoc: all
|
||||
def self.create_int_methods(klass, nbits, endian)
|
||||
max = (1 << (nbits - 1)) - 1
|
||||
min = -(max + 1)
|
||||
clamp = "val = (val < #{min}) ? #{min} : (val > #{max}) ? #{max} : val"
|
||||
|
||||
int2uint = "val = val & #{(1 << nbits) - 1}"
|
||||
uint2int = "val = ((val & #{1 << (nbits - 1)}).zero?) ? " +
|
||||
"val & #{max} : -(((~val) & #{max}) + 1)"
|
||||
|
||||
read = create_read_code(nbits, endian)
|
||||
to_s = create_to_s_code(nbits, endian)
|
||||
|
||||
define_methods(klass, nbits / 8, clamp, read, to_s, int2uint, uint2int)
|
||||
end
|
||||
|
||||
def self.create_uint_methods(klass, nbits, endian)
|
||||
min = 0
|
||||
max = (1 << nbits) - 1
|
||||
clamp = "val = (val < #{min}) ? #{min} : (val > #{max}) ? #{max} : val"
|
||||
|
||||
read = create_read_code(nbits, endian)
|
||||
to_s = create_to_s_code(nbits, endian)
|
||||
|
||||
define_methods(klass, nbits / 8, clamp, read, to_s)
|
||||
end
|
||||
|
||||
def self.create_read_code(nbits, endian)
|
||||
c16 = (endian == :little) ? 'v' : 'n'
|
||||
c32 = (endian == :little) ? 'V' : 'N'
|
||||
b1 = (endian == :little) ? 0 : 1
|
||||
b2 = (endian == :little) ? 1 : 0
|
||||
|
||||
case nbits
|
||||
when 8; "io.readbytes(1).unpack('C').at(0)"
|
||||
when 16; "io.readbytes(2).unpack('#{c16}').at(0)"
|
||||
when 32; "io.readbytes(4).unpack('#{c32}').at(0)"
|
||||
when 64; "(a = io.readbytes(8).unpack('#{c32 * 2}'); " +
|
||||
"(a.at(#{b2}) << 32) + a.at(#{b1}))"
|
||||
else
|
||||
raise "unknown nbits '#{nbits}'"
|
||||
end
|
||||
end
|
||||
|
||||
def self.create_to_s_code(nbits, endian)
|
||||
c16 = (endian == :little) ? 'v' : 'n'
|
||||
c32 = (endian == :little) ? 'V' : 'N'
|
||||
v1 = (endian == :little) ? 'val' : '(val >> 32)'
|
||||
v2 = (endian == :little) ? '(val >> 32)' : 'val'
|
||||
|
||||
case nbits
|
||||
when 8; "val.chr"
|
||||
when 16; "[val].pack('#{c16}')"
|
||||
when 32; "[val].pack('#{c32}')"
|
||||
when 64; "[#{v1} & 0xffffffff, #{v2} & 0xffffffff].pack('#{c32 * 2}')"
|
||||
else
|
||||
raise "unknown nbits '#{nbits}'"
|
||||
end
|
||||
end
|
||||
|
||||
def self.define_methods(klass, nbytes, clamp, read, to_s,
|
||||
int2uint = nil, uint2int = nil)
|
||||
# define methods in the given class
|
||||
klass.module_eval <<-END
|
||||
def value=(val)
|
||||
#{clamp}
|
||||
super(val)
|
||||
end
|
||||
|
||||
def _do_num_bytes(ignored)
|
||||
#{nbytes}
|
||||
end
|
||||
|
||||
#---------------
|
||||
private
|
||||
|
||||
def sensible_default
|
||||
0
|
||||
end
|
||||
|
||||
def val_to_str(val)
|
||||
#{clamp}
|
||||
#{int2uint unless int2uint.nil?}
|
||||
#{to_s}
|
||||
end
|
||||
|
||||
def read_val(io)
|
||||
val = #{read}
|
||||
#{uint2int unless uint2int.nil?}
|
||||
end
|
||||
END
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Unsigned 1 byte integer.
|
||||
class Uint8 < BinData::Single
|
||||
register(self.name, self)
|
||||
Integer.create_uint_methods(self, 8, :little)
|
||||
end
|
||||
|
||||
# Unsigned 2 byte little endian integer.
|
||||
class Uint16le < BinData::Single
|
||||
register(self.name, self)
|
||||
Integer.create_uint_methods(self, 16, :little)
|
||||
end
|
||||
|
||||
# Unsigned 2 byte big endian integer.
|
||||
class Uint16be < BinData::Single
|
||||
register(self.name, self)
|
||||
Integer.create_uint_methods(self, 16, :big)
|
||||
end
|
||||
|
||||
# Unsigned 4 byte little endian integer.
|
||||
class Uint32le < BinData::Single
|
||||
register(self.name, self)
|
||||
Integer.create_uint_methods(self, 32, :little)
|
||||
end
|
||||
|
||||
# Unsigned 4 byte big endian integer.
|
||||
class Uint32be < BinData::Single
|
||||
register(self.name, self)
|
||||
Integer.create_uint_methods(self, 32, :big)
|
||||
end
|
||||
|
||||
# Unsigned 8 byte little endian integer.
|
||||
class Uint64le < BinData::Single
|
||||
register(self.name, self)
|
||||
Integer.create_uint_methods(self, 64, :little)
|
||||
end
|
||||
|
||||
# Unsigned 8 byte big endian integer.
|
||||
class Uint64be < BinData::Single
|
||||
register(self.name, self)
|
||||
Integer.create_uint_methods(self, 64, :big)
|
||||
end
|
||||
|
||||
# Signed 1 byte integer.
|
||||
class Int8 < BinData::Single
|
||||
register(self.name, self)
|
||||
Integer.create_int_methods(self, 8, :little)
|
||||
end
|
||||
|
||||
# Signed 2 byte little endian integer.
|
||||
class Int16le < BinData::Single
|
||||
register(self.name, self)
|
||||
Integer.create_int_methods(self, 16, :little)
|
||||
end
|
||||
|
||||
# Signed 2 byte big endian integer.
|
||||
class Int16be < BinData::Single
|
||||
register(self.name, self)
|
||||
Integer.create_int_methods(self, 16, :big)
|
||||
end
|
||||
|
||||
# Signed 4 byte little endian integer.
|
||||
class Int32le < BinData::Single
|
||||
register(self.name, self)
|
||||
Integer.create_int_methods(self, 32, :little)
|
||||
end
|
||||
|
||||
# Signed 4 byte big endian integer.
|
||||
class Int32be < BinData::Single
|
||||
register(self.name, self)
|
||||
Integer.create_int_methods(self, 32, :big)
|
||||
end
|
||||
|
||||
# Signed 8 byte little endian integer.
|
||||
class Int64le < BinData::Single
|
||||
register(self.name, self)
|
||||
Integer.create_int_methods(self, 64, :little)
|
||||
end
|
||||
|
||||
# Signed 8 byte big endian integer.
|
||||
class Int64be < BinData::Single
|
||||
register(self.name, self)
|
||||
Integer.create_int_methods(self, 64, :big)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,192 @@
|
|||
module BinData
|
||||
# A wrapper around an IO object. The wrapper provides a consistent
|
||||
# interface for BinData objects to use when accessing the IO.
|
||||
class IO
|
||||
|
||||
# Create a new IO wrapper around +io+. +io+ must support #read if used
|
||||
# for reading, #write if used for writing, #pos if reading the current
|
||||
# stream position and #seek if setting the current stream position. If
|
||||
# +io+ is a string it will be automatically wrapped in an StringIO object.
|
||||
#
|
||||
# The IO can handle bitstreams in either big or little endian format.
|
||||
#
|
||||
# M byte1 L M byte2 L
|
||||
# S 76543210 S S fedcba98 S
|
||||
# B B B B
|
||||
#
|
||||
# In big endian format:
|
||||
# readbits(6), readbits(5) #=> [765432, 10fed]
|
||||
#
|
||||
# In little endian format:
|
||||
# readbits(6), readbits(5) #=> [543210, a9876]
|
||||
#
|
||||
def initialize(io)
|
||||
raise ArgumentError, "io must not be a BinData::IO" if BinData::IO === io
|
||||
|
||||
# wrap strings in a StringIO
|
||||
if io.respond_to?(:to_str)
|
||||
io = StringIO.new(io)
|
||||
end
|
||||
|
||||
@raw_io = io
|
||||
|
||||
# initial stream position if stream supports positioning
|
||||
@initial_pos = io.respond_to?(:pos) ? io.pos : 0
|
||||
|
||||
# bits when reading
|
||||
@rnbits = 0
|
||||
@rval = 0
|
||||
@rendian = nil
|
||||
|
||||
# bits when writing
|
||||
@wnbits = 0
|
||||
@wval = 0
|
||||
@wendian = nil
|
||||
end
|
||||
|
||||
# Access to the underlying raw io.
|
||||
attr_reader :raw_io
|
||||
|
||||
# Returns the current offset of the io stream. The exact value of
|
||||
# the offset when reading bitfields is not defined.
|
||||
def offset
|
||||
if @raw_io.respond_to?(:pos)
|
||||
@raw_io.pos - @initial_pos
|
||||
else
|
||||
# stream does not support positioning
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
# Seek +n+ bytes from the current position in the io stream.
|
||||
def seekbytes(n)
|
||||
@raw_io.seek(n, ::IO::SEEK_CUR)
|
||||
end
|
||||
|
||||
# Reads exactly +n+ bytes from +io+.
|
||||
#
|
||||
# If the data read is nil an EOFError is raised.
|
||||
#
|
||||
# If the data read is too short an IOError is raised.
|
||||
def readbytes(n)
|
||||
raise "Internal state error nbits = #{@rnbits}" if @rnbits > 8
|
||||
@rnbits = 0
|
||||
@rval = 0
|
||||
|
||||
str = @raw_io.read(n)
|
||||
raise EOFError, "End of file reached" if str.nil?
|
||||
raise IOError, "data truncated" if str.size < n
|
||||
str
|
||||
end
|
||||
|
||||
# Reads exactly +nbits+ bits from +io+. +endian+ specifies whether
|
||||
# the bits are stored in :big or :little endian format.
|
||||
def readbits(nbits, endian = :big)
|
||||
if @rendian != endian
|
||||
# don't mix bits of differing endian
|
||||
@rnbits = 0
|
||||
@rval = 0
|
||||
@rendian = endian
|
||||
end
|
||||
|
||||
while nbits > @rnbits
|
||||
byte = @raw_io.read(1)
|
||||
raise EOFError, "End of file reached" if byte.nil?
|
||||
byte = byte.unpack('C').at(0) & 0xff
|
||||
|
||||
if endian == :big
|
||||
@rval = (@rval << 8) | byte
|
||||
else
|
||||
@rval = @rval | (byte << @rnbits)
|
||||
end
|
||||
|
||||
@rnbits += 8
|
||||
end
|
||||
|
||||
if endian == :big
|
||||
val = (@rval >> (@rnbits - nbits)) & ((1 << nbits) - 1)
|
||||
@rnbits -= nbits
|
||||
@rval &= ((1 << @rnbits) - 1)
|
||||
else
|
||||
val = @rval & ((1 << nbits) - 1)
|
||||
@rnbits -= nbits
|
||||
@rval >>= nbits
|
||||
end
|
||||
|
||||
val
|
||||
end
|
||||
|
||||
# Writes the given string of bytes to the io stream.
|
||||
def writebytes(str)
|
||||
flushbits
|
||||
@raw_io.write(str)
|
||||
end
|
||||
|
||||
# Reads +nbits+ bits from +val+ to the stream. +endian+ specifies whether
|
||||
# the bits are to be stored in :big or :little endian format.
|
||||
def writebits(val, nbits, endian = :big)
|
||||
# clamp val to range
|
||||
val = val & ((1 << nbits) - 1)
|
||||
|
||||
if @wendian != endian
|
||||
# don't mix bits of differing endian
|
||||
flushbits if @wnbits > 0
|
||||
|
||||
@wendian = endian
|
||||
end
|
||||
|
||||
if endian == :big
|
||||
while nbits > 0
|
||||
bits_req = 8 - @wnbits
|
||||
if nbits >= bits_req
|
||||
msb_bits = (val >> (nbits - bits_req)) & ((1 << bits_req) - 1)
|
||||
nbits -= bits_req
|
||||
val &= (1 << nbits) - 1
|
||||
|
||||
@wval = (@wval << bits_req) | msb_bits
|
||||
@raw_io.write(@wval.chr)
|
||||
|
||||
@wval = 0
|
||||
@wnbits = 0
|
||||
else
|
||||
@wval = (@wval << nbits) | val
|
||||
@wnbits += nbits
|
||||
nbits = 0
|
||||
end
|
||||
end
|
||||
else
|
||||
while nbits > 0
|
||||
bits_req = 8 - @wnbits
|
||||
if nbits >= bits_req
|
||||
lsb_bits = val & ((1 << bits_req) - 1)
|
||||
nbits -= bits_req
|
||||
val >>= bits_req
|
||||
|
||||
@wval |= (lsb_bits << @wnbits)
|
||||
@raw_io.write(@wval.chr)
|
||||
|
||||
@wval = 0
|
||||
@wnbits = 0
|
||||
else
|
||||
@wval |= (val << @wnbits)
|
||||
@wnbits += nbits
|
||||
nbits = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# To be called after all +writebits+ have been applied.
|
||||
def flushbits
|
||||
if @wnbits > 8
|
||||
raise "Internal state error nbits = #{@wnbits}" if @wnbits > 8
|
||||
elsif @wnbits > 0
|
||||
writebits(0, 8 - @wnbits, @wendian)
|
||||
else
|
||||
# do nothing
|
||||
end
|
||||
end
|
||||
alias_method :flush, :flushbits
|
||||
|
||||
end
|
||||
end
|
|
@ -0,0 +1,111 @@
|
|||
module BinData
|
||||
# The enviroment in which a lazily evaluated lamba is called. These lambdas
|
||||
# are those that are passed to data objects as parameters. Each lambda
|
||||
# has access to the following:
|
||||
#
|
||||
# parent:: the environment of the parent data object
|
||||
# params:: any extra parameters that have been passed to the data object.
|
||||
# The value of a parameter is either a lambda, a symbol or a
|
||||
# literal value (such as a Fixnum).
|
||||
#
|
||||
# Unknown methods are resolved in the context of the parent environment,
|
||||
# first as keys in the extra parameters, and secondly as methods in the
|
||||
# parent data object. This makes the lambda easier to read as we just write
|
||||
# <tt>field</tt> instead of <tt>obj.field</tt>.
|
||||
class LazyEvalEnv
|
||||
# An empty hash shared by all instances
|
||||
@@empty_hash = Hash.new.freeze
|
||||
|
||||
@@variables_cache = {}
|
||||
|
||||
# Creates a new environment. +parent+ is the environment of the
|
||||
# parent data object.
|
||||
def initialize(parent = nil)
|
||||
@parent = parent
|
||||
@variables = @@empty_hash
|
||||
@overrides = @@empty_hash
|
||||
@params = @@empty_hash
|
||||
end
|
||||
attr_reader :parent, :params
|
||||
attr_accessor :data_object
|
||||
|
||||
# only accessible by another LazyEvalEnv
|
||||
protected :data_object
|
||||
|
||||
# Set the parameters for this environment.
|
||||
def params=(p)
|
||||
@params = (p.nil? or p.empty?) ? @@empty_hash : p
|
||||
end
|
||||
|
||||
# Add a variable with a pre-assigned value to this environment. +sym+
|
||||
# will be accessible as a variable for any lambda evaluated
|
||||
# with #lazy_eval.
|
||||
def add_variable(sym, value)
|
||||
sym = sym.to_sym
|
||||
if @variables.equal?(@@empty_hash)
|
||||
# optimise the case where only 1 variable is added as this
|
||||
# is the most common occurance (BinData::Arrays adding index)
|
||||
key = [sym, value]
|
||||
@variables = @@variables_cache[key]
|
||||
if @variables.nil?
|
||||
# cache this variable and value so it can be shared with
|
||||
# other LazyEvalEnvs to keep memory usage down
|
||||
@variables = {sym => value}.freeze
|
||||
@@variables_cache[key] = @variables
|
||||
end
|
||||
else
|
||||
if @variables.length == 1
|
||||
key = @variables.keys[0]
|
||||
@variables = {key => @variables[key]}
|
||||
end
|
||||
@variables[sym] = value
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: offset_of needs to be better thought out
|
||||
def offset_of(sym)
|
||||
@parent.data_object.offset_of(sym)
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
|
||||
# Returns the data_object for the parent environment.
|
||||
def parent_data_object
|
||||
@parent.nil? ? nil : @parent.data_object
|
||||
end
|
||||
|
||||
# Evaluates +obj+ in the context of this environment. Evaluation
|
||||
# recurses until it yields a value that is not a symbol or lambda.
|
||||
# +overrides+ is an optional +params+ like hash
|
||||
def lazy_eval(obj, overrides = nil)
|
||||
result = obj
|
||||
@overrides = overrides if overrides
|
||||
if obj.is_a? Symbol
|
||||
# treat :foo as lambda { foo }
|
||||
result = __send__(obj)
|
||||
elsif obj.respond_to? :arity
|
||||
result = instance_eval(&obj)
|
||||
end
|
||||
@overrides = @@empty_hash
|
||||
result
|
||||
end
|
||||
|
||||
def method_missing(symbol, *args)
|
||||
if @overrides.include?(symbol)
|
||||
@overrides[symbol]
|
||||
elsif @variables.include?(symbol)
|
||||
@variables[symbol]
|
||||
elsif @parent
|
||||
obj = symbol
|
||||
if @parent.params and @parent.params.has_key?(symbol)
|
||||
obj = @parent.params[symbol]
|
||||
elsif @parent.data_object and @parent.data_object.respond_to?(symbol)
|
||||
obj = @parent.data_object.__send__(symbol, *args)
|
||||
end
|
||||
@parent.lazy_eval(obj)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,134 @@
|
|||
require 'bindata/struct'
|
||||
|
||||
module BinData
|
||||
# A MultiValue is a declarative wrapper around Struct.
|
||||
#
|
||||
# require 'bindata'
|
||||
#
|
||||
# class Tuple < BinData::MultiValue
|
||||
# int8 :x
|
||||
# int8 :y
|
||||
# int8 :z
|
||||
# end
|
||||
#
|
||||
# class SomeDataType < BinData::MultiValue
|
||||
# hide 'a'
|
||||
#
|
||||
# int32le :a
|
||||
# int16le :b
|
||||
# tuple :s
|
||||
# end
|
||||
#
|
||||
# obj = SomeDataType.new
|
||||
# obj.field_names =># ["b", "s"]
|
||||
#
|
||||
#
|
||||
# == Parameters
|
||||
#
|
||||
# Parameters may be provided at initialisation to control the behaviour of
|
||||
# an object. These params are:
|
||||
#
|
||||
# <tt>:fields</tt>:: An array specifying the fields for this struct.
|
||||
# Each element of the array is of the form [type, name,
|
||||
# params]. Type is a symbol representing a registered
|
||||
# type. Name is the name of this field. Params is an
|
||||
# optional hash of parameters to pass to this field
|
||||
# when instantiating it.
|
||||
# <tt>:hide</tt>:: A list of the names of fields that are to be hidden
|
||||
# from the outside world. Hidden fields don't appear
|
||||
# in #snapshot or #field_names but are still accessible
|
||||
# by name.
|
||||
# <tt>:endian</tt>:: Either :little or :big. This specifies the default
|
||||
# endian of any numerics in this struct, or in any
|
||||
# nested data objects.
|
||||
class MultiValue < BinData::Struct
|
||||
|
||||
class << self
|
||||
# Register the names of all subclasses of this class.
|
||||
def inherited(subclass) #:nodoc:
|
||||
register(subclass.name, subclass)
|
||||
end
|
||||
|
||||
# Returns or sets the endianess of numerics used in this stucture.
|
||||
# Endianess is applied to the fields of this structure.
|
||||
# Valid values are :little and :big.
|
||||
def endian(endian = nil)
|
||||
@endian ||= nil
|
||||
if [:little, :big].include?(endian)
|
||||
@endian = endian
|
||||
elsif endian != nil
|
||||
raise ArgumentError, "unknown value for endian '#{endian}'"
|
||||
end
|
||||
@endian
|
||||
end
|
||||
|
||||
# Returns the names of any hidden fields in this struct. Any given args
|
||||
# are appended to the hidden list.
|
||||
def hide(*args)
|
||||
# note that fields are stored in an instance variable not a class var
|
||||
@hide ||= []
|
||||
@hide.concat(args.collect { |name| name.to_s })
|
||||
@hide
|
||||
end
|
||||
|
||||
# Returns all stored fields.
|
||||
# Should only be called by #sanitize_parameters
|
||||
def fields
|
||||
@fields ||= []
|
||||
end
|
||||
|
||||
# Used to define fields for this structure.
|
||||
def method_missing(symbol, *args)
|
||||
name, params = args
|
||||
|
||||
type = symbol
|
||||
name = name.to_s
|
||||
params ||= {}
|
||||
|
||||
# note that fields are stored in an instance variable not a class var
|
||||
@fields ||= []
|
||||
|
||||
# check that type is known
|
||||
unless Sanitizer.type_exists?(type, endian)
|
||||
raise TypeError, "unknown type '#{type}' for #{self}", caller
|
||||
end
|
||||
|
||||
# check for duplicate names
|
||||
@fields.each do |t, n, p|
|
||||
if n == name
|
||||
raise SyntaxError, "duplicate field '#{name}' in #{self}", caller
|
||||
end
|
||||
end
|
||||
|
||||
# check that name doesn't shadow an existing method
|
||||
if self.instance_methods.include?(name)
|
||||
raise NameError.new("", name),
|
||||
"field '#{name}' shadows an existing method", caller
|
||||
end
|
||||
|
||||
# check that name isn't reserved
|
||||
if self::RESERVED.include?(name)
|
||||
raise NameError.new("", name),
|
||||
"field '#{name}' is a reserved name", caller
|
||||
end
|
||||
|
||||
# remember this field. These fields will be recalled upon creating
|
||||
# an instance of this class
|
||||
@fields.push([type, name, params])
|
||||
end
|
||||
|
||||
# Ensures that +params+ is of the form expected by #initialize.
|
||||
def sanitize_parameters!(sanitizer, params)
|
||||
endian = params[:endian] || self.endian
|
||||
fields = params[:fields] || self.fields
|
||||
hide = params[:hide] || self.hide
|
||||
|
||||
params[:endian] = endian unless endian.nil?
|
||||
params[:fields] = fields
|
||||
params[:hide] = hide
|
||||
|
||||
super(sanitizer, params)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,341 @@
|
|||
require 'bindata/base'
|
||||
require 'bindata/sanitize'
|
||||
|
||||
module BinData
|
||||
# An Array is a list of data objects of the same type.
|
||||
#
|
||||
# require 'bindata'
|
||||
#
|
||||
# data = "\x03\x04\x05\x06\x07\x08\x09"
|
||||
#
|
||||
# obj = BinData::Array.new(:type => :int8, :initial_length => 6)
|
||||
# obj.read(data)
|
||||
# obj.snapshot #=> [3, 4, 5, 6, 7, 8]
|
||||
#
|
||||
# obj = BinData::Array.new(:type => :int8,
|
||||
# :read_until => lambda { index == 1 })
|
||||
# obj.read(data)
|
||||
# obj.snapshot #=> [3, 4]
|
||||
#
|
||||
# obj = BinData::Array.new(:type => :int8,
|
||||
# :read_until => lambda { element >= 6 })
|
||||
# obj.read(data)
|
||||
# obj.snapshot #=> [3, 4, 5, 6]
|
||||
#
|
||||
# obj = BinData::Array.new(:type => :int8,
|
||||
# :read_until => lambda { array[index] + array[index - 1] == 13 })
|
||||
# obj.read(data)
|
||||
# obj.snapshot #=> [3, 4, 5, 6, 7]
|
||||
#
|
||||
# obj = BinData::Array.new(:type => :int8, :read_until_eof => true)
|
||||
# obj.read(data)
|
||||
# obj.snapshot #=> [3, 4, 5, 6, 7, 8, 9]
|
||||
#
|
||||
#
|
||||
# == Parameters
|
||||
#
|
||||
# Parameters may be provided at initialisation to control the behaviour of
|
||||
# an object. These params are:
|
||||
#
|
||||
# <tt>:type</tt>:: The symbol representing the data type of the
|
||||
# array elements. If the type is to have params
|
||||
# passed to it, then it should be provided as
|
||||
# <tt>[type_symbol, hash_params]</tt>.
|
||||
# <tt>:initial_length</tt>:: The initial length of the array.
|
||||
# <tt>:read_until</tt>:: While reading, elements are read until this
|
||||
# condition is true. This is typically used to
|
||||
# read an array until a sentinel value is found.
|
||||
# The variables +index+, +element+ and +array+
|
||||
# are made available to any lambda assigned to
|
||||
# this parameter.
|
||||
#
|
||||
# Each data object in an array has the variable +index+ made available
|
||||
# to any lambda evaluated as a parameter of that data object.
|
||||
class Array < BinData::Base
|
||||
include Enumerable
|
||||
|
||||
# Register this class
|
||||
register(self.name, self)
|
||||
|
||||
# These are the parameters used by this class.
|
||||
mandatory_parameter :type
|
||||
optional_parameters :initial_length, :read_until, :read_until_eof
|
||||
mutually_exclusive_parameters :initial_length, :read_until, :read_until_eof
|
||||
|
||||
class << self
|
||||
# Returns a sanitized +params+ that is of the form expected
|
||||
# by #initialize.
|
||||
def sanitize_parameters(sanitizer, params)
|
||||
params = params.dup
|
||||
|
||||
unless params.has_key?(:initial_length) or params.has_key?(:read_until) or params.has_key?(:read_until_eof)
|
||||
# ensure one of :initial_length, :read_until, or :read_until_eof exists
|
||||
params[:initial_length] = 0
|
||||
end
|
||||
|
||||
if params.has_key?(:read_length)
|
||||
warn ":read_length is not used with arrays. You probably want to change this to :initial_length"
|
||||
end
|
||||
|
||||
if params.has_key?(:type)
|
||||
type, el_params = params[:type]
|
||||
params[:type] = sanitizer.sanitize(type, el_params)
|
||||
end
|
||||
|
||||
super(sanitizer, params)
|
||||
end
|
||||
end
|
||||
|
||||
# Creates a new Array
|
||||
def initialize(params = {}, env = nil)
|
||||
super(params, env)
|
||||
|
||||
klass, el_params = param(:type)
|
||||
|
||||
@element_list = nil
|
||||
@element_klass = klass
|
||||
@element_params = el_params
|
||||
end
|
||||
|
||||
# Returns if the element at position +index+ is clear?. If +index+
|
||||
# is not given, then returns whether all fields are clear.
|
||||
def clear?(index = nil)
|
||||
if @element_list.nil?
|
||||
true
|
||||
elsif index.nil?
|
||||
elements.each { |f| return false if not f.clear? }
|
||||
true
|
||||
else
|
||||
(index < elements.length) ? elements[index].clear? : true
|
||||
end
|
||||
end
|
||||
|
||||
# Clears the element at position +index+. If +index+ is not given, then
|
||||
# the internal state of the array is reset to that of a newly created
|
||||
# object.
|
||||
def clear(index = nil)
|
||||
if @element_list.nil?
|
||||
# do nothing as the array is already clear
|
||||
elsif index.nil?
|
||||
@element_list = nil
|
||||
elsif index < elements.length
|
||||
elements[index].clear
|
||||
end
|
||||
end
|
||||
|
||||
# Returns whether this data object contains a single value. Single
|
||||
# value data objects respond to <tt>#value</tt> and <tt>#value=</tt>.
|
||||
def single_value?
|
||||
return false
|
||||
end
|
||||
|
||||
# To be called after calling #do_read.
|
||||
def done_read
|
||||
elements.each { |f| f.done_read }
|
||||
end
|
||||
|
||||
# Appends a new element to the end of the array. If the array contains
|
||||
# single_values then the +value+ may be provided to the call.
|
||||
# Returns the appended object, or value in the case of single_values.
|
||||
def append(value = nil)
|
||||
# TODO: deprecate #append as it can be replaced with #push
|
||||
append_new_element
|
||||
self[-1] = value unless value.nil?
|
||||
self.last
|
||||
end
|
||||
|
||||
# Pushes the given object(s) on to the end of this array.
|
||||
# This expression returns the array itself, so several appends may
|
||||
# be chained together.
|
||||
def push(*args)
|
||||
args.each do |arg|
|
||||
if @element_klass == arg.class
|
||||
# TODO: need to modify arg.env to add_variable(:index) and
|
||||
# to link arg.env to self.env
|
||||
elements.push(arg)
|
||||
else
|
||||
append(arg)
|
||||
end
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
# Returns the element at +index+. If the element is a single_value
|
||||
# then the value of the element is returned instead.
|
||||
def [](*args)
|
||||
if args.length == 1 and ::Integer === args[0]
|
||||
# extend array automatically
|
||||
while args[0] >= elements.length
|
||||
append_new_element
|
||||
end
|
||||
end
|
||||
|
||||
data = elements[*args]
|
||||
if data.respond_to?(:each)
|
||||
data.collect { |el| (el && el.single_value?) ? el.value : el }
|
||||
else
|
||||
(data && data.single_value?) ? data.value : data
|
||||
end
|
||||
end
|
||||
alias_method :slice, :[]
|
||||
|
||||
# Sets the element at +index+. If the element is a single_value
|
||||
# then the value of the element is set instead.
|
||||
def []=(index, value)
|
||||
# extend array automatically
|
||||
while index >= elements.length
|
||||
append_new_element
|
||||
end
|
||||
|
||||
obj = elements[index]
|
||||
unless obj.single_value?
|
||||
# TODO: allow setting objects, not just values
|
||||
raise NoMethodError, "undefined method `[]=' for #{self}", caller
|
||||
end
|
||||
obj.value = value
|
||||
end
|
||||
|
||||
# Iterate over each element in the array. If the elements are
|
||||
# single_values then the values of the elements are iterated instead.
|
||||
def each
|
||||
elements.each do |el|
|
||||
yield(el.single_value? ? el.value : el)
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the first element, or the first +n+ elements, of the array.
|
||||
# If the array is empty, the first form returns nil, and the second
|
||||
# form returns an empty array.
|
||||
def first(n = nil)
|
||||
if n.nil?
|
||||
if elements.empty?
|
||||
# explicitly return nil as arrays grow automatically
|
||||
nil
|
||||
else
|
||||
self[0]
|
||||
end
|
||||
else
|
||||
self[0, n]
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the last element, or the last +n+ elements, of the array.
|
||||
# If the array is empty, the first form returns nil, and the second
|
||||
# form returns an empty array.
|
||||
def last(n = nil)
|
||||
if n.nil?
|
||||
self[-1]
|
||||
else
|
||||
n = length if n > length
|
||||
self[-n, n]
|
||||
end
|
||||
end
|
||||
|
||||
# The number of elements in this array.
|
||||
def length
|
||||
elements.length
|
||||
end
|
||||
alias_method :size, :length
|
||||
|
||||
# Returns true if self array contains no elements.
|
||||
def empty?
|
||||
length.zero?
|
||||
end
|
||||
|
||||
# Allow this object to be used in array context.
|
||||
def to_ary
|
||||
snapshot
|
||||
end
|
||||
|
||||
#---------------
|
||||
private
|
||||
|
||||
# Reads the values for all fields in this object from +io+.
|
||||
def _do_read(io)
|
||||
if has_param?(:initial_length)
|
||||
elements.each { |f| f.do_read(io) }
|
||||
elsif has_param?(:read_until)
|
||||
@element_list = nil
|
||||
loop do
|
||||
element = append_new_element
|
||||
element.do_read(io)
|
||||
variables = { :index => self.length - 1, :element => self.last,
|
||||
:array => self }
|
||||
finished = eval_param(:read_until, variables)
|
||||
break if finished
|
||||
end
|
||||
else # :read_until_eof
|
||||
loop do
|
||||
element = append_new_element
|
||||
begin
|
||||
element.do_read(io)
|
||||
rescue EOFError
|
||||
finished = true
|
||||
remove_last_element
|
||||
end
|
||||
variables = { :index => self.length - 1, :element => self.last,
|
||||
:array => self }
|
||||
break if finished
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Writes the values for all fields in this object to +io+.
|
||||
def _do_write(io)
|
||||
elements.each { |f| f.do_write(io) }
|
||||
end
|
||||
|
||||
# Returns the number of bytes it will take to write the element at
|
||||
# +index+. If +index+, then returns the number of bytes required
|
||||
# to write all fields.
|
||||
def _do_num_bytes(index)
|
||||
if index.nil?
|
||||
(elements.inject(0) { |sum, f| sum + f.do_num_bytes }).ceil
|
||||
else
|
||||
elements[index].do_num_bytes
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a snapshot of the data in this array.
|
||||
def _snapshot
|
||||
elements.collect { |e| e.snapshot }
|
||||
end
|
||||
|
||||
# Returns the list of all elements in the array. The elements
|
||||
# will be instantiated on the first call to this method.
|
||||
def elements
|
||||
if @element_list.nil?
|
||||
@element_list = []
|
||||
if has_param?(:initial_length)
|
||||
# create the desired number of instances
|
||||
eval_param(:initial_length).times do
|
||||
append_new_element
|
||||
end
|
||||
end
|
||||
end
|
||||
@element_list
|
||||
end
|
||||
|
||||
# Creates a new element and appends it to the end of @element_list.
|
||||
# Returns the newly created element
|
||||
def append_new_element
|
||||
# ensure @element_list is initialised
|
||||
elements()
|
||||
|
||||
env = create_env
|
||||
env.add_variable(:index, @element_list.length)
|
||||
element = @element_klass.new(@element_params, env)
|
||||
@element_list << element
|
||||
element
|
||||
end
|
||||
|
||||
# Pops the last element off the end of @element_list.
|
||||
# Returns the popped element.
|
||||
# This is important for the :read_until_eof option to properly close
|
||||
# the do_read io handle.
|
||||
def remove_last_element
|
||||
elements()
|
||||
@element_list.pop
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,92 @@
|
|||
Index: lib/bindata/array.rb
|
||||
===================================================================
|
||||
--- lib/bindata/array.rb (revision 94)
|
||||
+++ lib/bindata/array.rb (working copy)
|
||||
@@ -27,6 +27,10 @@
|
||||
# obj.read(data)
|
||||
# obj.snapshot #=> [3, 4, 5, 6, 7]
|
||||
#
|
||||
+ # obj = BinData::Array.new(:type => :int8, :read_until => :eof)
|
||||
+ # obj.read(data)
|
||||
+ # obj.snapshot #=> [3, 4, 5, 6, 7, 8, 9]
|
||||
+ #
|
||||
# == Parameters
|
||||
#
|
||||
# Parameters may be provided at initialisation to control the behaviour of
|
||||
@@ -42,7 +46,9 @@
|
||||
# read an array until a sentinel value is found.
|
||||
# The variables +index+, +element+ and +array+
|
||||
# are made available to any lambda assigned to
|
||||
- # this parameter.
|
||||
+ # this parameter. If the value of this parameter
|
||||
+ # is the symbol :eof, then the array will read
|
||||
+ # as much data from the stream as possible.
|
||||
#
|
||||
# Each data object in an array has the variable +index+ made available
|
||||
# to any lambda evaluated as a parameter of that data object.
|
||||
@@ -249,15 +255,28 @@
|
||||
def _do_read(io)
|
||||
if has_param?(:initial_length)
|
||||
elements.each { |f| f.do_read(io) }
|
||||
- else # :read_until
|
||||
- @element_list = nil
|
||||
- loop do
|
||||
- element = append_new_element
|
||||
- element.do_read(io)
|
||||
- variables = { :index => self.length - 1, :element => self.last,
|
||||
- :array => self }
|
||||
- finished = eval_param(:read_until, variables)
|
||||
- break if finished
|
||||
+ elsif has_param?(:read_until)
|
||||
+ if param(:read_until) == :eof
|
||||
+ @element_list = nil
|
||||
+ loop do
|
||||
+ element = append_new_element
|
||||
+ begin
|
||||
+ element.do_read(io)
|
||||
+ rescue
|
||||
+ @element_list.pop
|
||||
+ break
|
||||
+ end
|
||||
+ end
|
||||
+ else
|
||||
+ @element_list = nil
|
||||
+ loop do
|
||||
+ element = append_new_element
|
||||
+ element.do_read(io)
|
||||
+ variables = { :index => self.length - 1, :element => self.last,
|
||||
+ :array => self }
|
||||
+ finished = eval_param(:read_until, variables)
|
||||
+ break if finished
|
||||
+ end
|
||||
end
|
||||
end
|
||||
end
|
||||
Index: spec/array_spec.rb
|
||||
===================================================================
|
||||
--- spec/array_spec.rb (revision 92)
|
||||
+++ spec/array_spec.rb (working copy)
|
||||
@@ -264,6 +264,22 @@
|
||||
end
|
||||
end
|
||||
|
||||
+describe BinData::Array, "with :read_until => :eof" do
|
||||
+ it "should read records until eof" do
|
||||
+ obj = BinData::Array.new(:type => :int8, :read_until => :eof)
|
||||
+ data = "\x01\x02\x03"
|
||||
+ obj.read(data)
|
||||
+ obj.snapshot.should == [1, 2, 3]
|
||||
+ end
|
||||
+
|
||||
+ it "should read records until eof, ignoring partial records" do
|
||||
+ obj = BinData::Array.new(:type => :int16be, :read_until => :eof)
|
||||
+ data = "\x00\x01\x00\x02\x03"
|
||||
+ obj.read(data)
|
||||
+ obj.snapshot.should == [1, 2]
|
||||
+ end
|
||||
+end
|
||||
+
|
||||
describe BinData::Array, "of bits" do
|
||||
before(:each) do
|
||||
@data = BinData::Array.new(:type => :bit1, :initial_length => 15)
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
Index: array.rb
|
||||
===================================================================
|
||||
--- array.rb (revision 98)
|
||||
+++ array.rb (working copy)
|
||||
@@ -26,7 +26,12 @@
|
||||
# :read_until => lambda { array[index] + array[index - 1] == 13 })
|
||||
# obj.read(data)
|
||||
# obj.snapshot #=> [3, 4, 5, 6, 7]
|
||||
- #
|
||||
+ #
|
||||
+ # obj = BinData::Array.new(:type => :int8, :read_until_eof => true)
|
||||
+ # obj.read(data)
|
||||
+ # obj.snapshot #=> [3, 4, 5, 6, 7, 8, 9]
|
||||
+ #
|
||||
+ #
|
||||
# == Parameters
|
||||
#
|
||||
# Parameters may be provided at initialisation to control the behaviour of
|
||||
@@ -54,8 +59,8 @@
|
||||
|
||||
# These are the parameters used by this class.
|
||||
mandatory_parameter :type
|
||||
- optional_parameters :initial_length, :read_until
|
||||
- mutually_exclusive_parameters :initial_length, :read_until
|
||||
+ optional_parameters :initial_length, :read_until, :read_until_eof
|
||||
+ mutually_exclusive_parameters :initial_length, :read_until, :read_until_eof
|
||||
|
||||
class << self
|
||||
# Returns a sanitized +params+ that is of the form expected
|
||||
@@ -63,8 +68,8 @@
|
||||
def sanitize_parameters(sanitizer, params)
|
||||
params = params.dup
|
||||
|
||||
- unless params.has_key?(:initial_length) or params.has_key?(:read_until)
|
||||
- # ensure one of :initial_length and :read_until exists
|
||||
+ unless params.has_key?(:initial_length) or params.has_key?(:read_until) or params.has_key?(:read_until_eof)
|
||||
+ # ensure one of :initial_length, :read_until, or :read_until_eof exists
|
||||
params[:initial_length] = 0
|
||||
end
|
||||
|
||||
@@ -249,7 +254,7 @@
|
||||
def _do_read(io)
|
||||
if has_param?(:initial_length)
|
||||
elements.each { |f| f.do_read(io) }
|
||||
- else # :read_until
|
||||
+ elsif has_param?(:read_until)
|
||||
@element_list = nil
|
||||
loop do
|
||||
element = append_new_element
|
||||
@@ -259,7 +264,20 @@
|
||||
finished = eval_param(:read_until, variables)
|
||||
break if finished
|
||||
end
|
||||
- end
|
||||
+ else # :read_until_eof
|
||||
+ loop do
|
||||
+ element = append_new_element
|
||||
+ begin
|
||||
+ element.do_read(io)
|
||||
+ rescue EOFError
|
||||
+ finished = true
|
||||
+ remove_last_element
|
||||
+ end
|
||||
+ variables = { :index => self.length - 1, :element => self.last,
|
||||
+ :array => self }
|
||||
+ break if finished
|
||||
+ end
|
||||
+ end
|
||||
end
|
||||
|
||||
# Writes the values for all fields in this object to +io+.
|
||||
@@ -310,5 +328,14 @@
|
||||
@element_list << element
|
||||
element
|
||||
end
|
||||
+
|
||||
+ # Pops the last element off the end of @element_list.
|
||||
+ # Returns the popped element.
|
||||
+ # This is important for the :read_until_eof option to properly close
|
||||
+ # the do_read io handle.
|
||||
+ def remove_last_element
|
||||
+ elements()
|
||||
+ @element_list.pop
|
||||
+ end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,42 @@
|
|||
require 'singleton'
|
||||
|
||||
module BinData
|
||||
# This registry contains a register of name -> class mappings.
|
||||
class Registry
|
||||
include Singleton
|
||||
|
||||
def initialize
|
||||
@registry = {}
|
||||
end
|
||||
|
||||
# Convert camelCase +name+ to underscore style.
|
||||
def underscore_name(name)
|
||||
name.to_s.sub(/.*::/, "").
|
||||
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
||||
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
||||
tr("-", "_").
|
||||
downcase
|
||||
end
|
||||
|
||||
# Registers the mapping of +name+ to +klass+. +name+ is converted
|
||||
# from camelCase to underscore style.
|
||||
# Returns the converted name
|
||||
def register(name, klass)
|
||||
# convert camelCase name to underscore style
|
||||
key = underscore_name(name)
|
||||
|
||||
# warn if replacing an existing class
|
||||
if $VERBOSE and (existing = @registry[key])
|
||||
warn "warning: replacing registered class #{existing} with #{klass}"
|
||||
end
|
||||
|
||||
@registry[key] = klass
|
||||
key.dup
|
||||
end
|
||||
|
||||
# Returns the class matching a previously registered +name+.
|
||||
def lookup(name)
|
||||
@registry[name.to_s]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
require "bindata/single"
|
||||
|
||||
module BinData
|
||||
# Rest will consume the input stream from the current position to the end of
|
||||
# the stream. This will mainly be useful for debugging and developing.
|
||||
#
|
||||
# require 'bindata'
|
||||
#
|
||||
# class A < BinData::MultiValue
|
||||
# string :a, :read_length => 5
|
||||
# rest :rest
|
||||
# end
|
||||
#
|
||||
# obj = A.read("abcdefghij")
|
||||
# obj.a #=> "abcde"
|
||||
# obj.rest #=" "fghij"
|
||||
#
|
||||
class Rest < BinData::Single
|
||||
|
||||
# Register this class
|
||||
register(self.name, self)
|
||||
|
||||
#---------------
|
||||
private
|
||||
|
||||
# Return the string representation that +val+ will take when written.
|
||||
def val_to_str(val)
|
||||
val
|
||||
end
|
||||
|
||||
# Read a number of bytes from +io+ and return the value they represent.
|
||||
def read_val(io)
|
||||
io.raw_io.read
|
||||
end
|
||||
|
||||
# Returns an empty string as default.
|
||||
def sensible_default
|
||||
""
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,121 @@
|
|||
require 'forwardable'
|
||||
|
||||
module BinData
|
||||
class Sanitizer
|
||||
class << self
|
||||
# Sanitize +params+ for +obj+.
|
||||
# Returns sanitized parameters.
|
||||
def sanitize(obj, params)
|
||||
sanitizer = self.new
|
||||
klass, new_params = sanitizer.sanitize(obj.class, params)
|
||||
new_params
|
||||
end
|
||||
|
||||
# Returns true if +type+ is registered.
|
||||
def type_exists?(type, endian = nil)
|
||||
lookup(type, endian) != nil
|
||||
end
|
||||
|
||||
# Returns the class matching a previously registered +name+.
|
||||
def lookup(name, endian)
|
||||
name = name.to_s
|
||||
klass = Registry.instance.lookup(name)
|
||||
if klass.nil? and endian != nil
|
||||
# lookup failed so attempt endian lookup
|
||||
if /^u?int\d{1,3}$/ =~ name
|
||||
new_name = name + ((endian == :little) ? "le" : "be")
|
||||
klass = Registry.instance.lookup(new_name)
|
||||
elsif ["float", "double"].include?(name)
|
||||
new_name = name + ((endian == :little) ? "_le" : "_be")
|
||||
klass = Registry.instance.lookup(new_name)
|
||||
end
|
||||
end
|
||||
klass
|
||||
end
|
||||
end
|
||||
|
||||
# Create a new Sanitizer.
|
||||
def initialize
|
||||
@seen = []
|
||||
@endian = nil
|
||||
end
|
||||
|
||||
# Executes the given block with +endian+ set as the current endian.
|
||||
def with_endian(endian, &block)
|
||||
if endian != nil
|
||||
saved_endian = @endian
|
||||
@endian = endian
|
||||
yield
|
||||
@endian = saved_endian
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
# Sanitizes +params+ for +type+.
|
||||
# Returns [klass, sanitized_params]
|
||||
def sanitize(type, params)
|
||||
if Class === type
|
||||
klass = type
|
||||
else
|
||||
klass = self.class.lookup(type, @endian)
|
||||
raise TypeError, "unknown type '#{type}'" if klass.nil?
|
||||
end
|
||||
|
||||
new_params = params.nil? ? {} : params.dup
|
||||
|
||||
if @seen.include?(klass)
|
||||
# This klass is defined recursively. Remember the current endian
|
||||
# and delay sanitizing the parameters until later.
|
||||
if @endian != nil and klass.accepted_parameters.include?(:endian) and
|
||||
not new_params.has_key?(:endian)
|
||||
new_params[:endian] = @endian
|
||||
end
|
||||
else
|
||||
# subclasses of MultiValue may be defined recursively
|
||||
# TODO: define a class field instead
|
||||
possibly_recursive = (BinData.const_defined?(:MultiValue) and
|
||||
klass.ancestors.include?(BinData.const_get(:MultiValue)))
|
||||
@seen.push(klass) if possibly_recursive
|
||||
|
||||
klass.sanitize_parameters!(self, new_params)
|
||||
new_params = SanitizedParameters.new(klass, new_params)
|
||||
end
|
||||
|
||||
[klass, new_params]
|
||||
end
|
||||
end
|
||||
|
||||
# A BinData object accepts arbitrary parameters. This class ensures that
|
||||
# the parameters have been sanitized, and categorizes them according to
|
||||
# whether they are BinData::Base.accepted_parameters or are extra.
|
||||
class SanitizedParameters
|
||||
extend Forwardable
|
||||
|
||||
# Sanitize the given parameters.
|
||||
def initialize(klass, params)
|
||||
@hash = params
|
||||
@accepted_parameters = {}
|
||||
@extra_parameters = {}
|
||||
|
||||
# partition parameters into known and extra parameters
|
||||
@hash.each do |k,v|
|
||||
k = k.to_sym
|
||||
if v.nil?
|
||||
raise ArgumentError, "parameter :#{k} has nil value in #{klass}"
|
||||
end
|
||||
|
||||
if klass.accepted_parameters.include?(k)
|
||||
@accepted_parameters[k] = v
|
||||
else
|
||||
@extra_parameters[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :accepted_parameters, :extra_parameters
|
||||
|
||||
def_delegators :@hash, :[], :has_key?, :include?, :keys
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
require 'bindata/base'
|
||||
|
||||
module BinData
|
||||
# A BinData::Single object is a container for a value that has a particular
|
||||
# binary representation. A value corresponds to a primitive type such as
|
||||
# as integer, float or string. Only one value can be contained by this
|
||||
# object. This value can be read from or written to an IO stream.
|
||||
#
|
||||
# require 'bindata'
|
||||
#
|
||||
# obj = BinData::Uint8.new(:initial_value => 42)
|
||||
# obj.value #=> 42
|
||||
# obj.value = 5
|
||||
# obj.value #=> 5
|
||||
# obj.clear
|
||||
# obj.value #=> 42
|
||||
#
|
||||
# obj = BinData::Uint8.new(:value => 42)
|
||||
# obj.value #=> 42
|
||||
# obj.value = 5
|
||||
# obj.value #=> 42
|
||||
#
|
||||
# obj = BinData::Uint8.new(:check_value => 3)
|
||||
# obj.read("\005") #=> BinData::ValidityError: value is '5' but expected '3'
|
||||
#
|
||||
# obj = BinData::Uint8.new(:check_value => lambda { value < 5 })
|
||||
# obj.read("\007") #=> BinData::ValidityError: value not as expected
|
||||
#
|
||||
# == Parameters
|
||||
#
|
||||
# Parameters may be provided at initialisation to control the behaviour of
|
||||
# an object. These params include those for BinData::Base as well as:
|
||||
#
|
||||
# [<tt>:initial_value</tt>] This is the initial value to use before one is
|
||||
# either #read or explicitly set with #value=.
|
||||
# [<tt>:value</tt>] The object will always have this value.
|
||||
# Explicitly calling #value= is prohibited when
|
||||
# using this param. In the interval between
|
||||
# calls to #do_read and #done_read, #value
|
||||
# will return the value of the data read from the
|
||||
# IO, not the result of the <tt>:value</tt> param.
|
||||
# [<tt>:check_value</tt>] Raise an error unless the value read in meets
|
||||
# this criteria. The variable +value+ is made
|
||||
# available to any lambda assigned to this
|
||||
# parameter. A boolean return indicates success
|
||||
# or failure. Any other return is compared to
|
||||
# the value just read in.
|
||||
class Single < BinData::Base
|
||||
# These are the parameters used by this class.
|
||||
optional_parameters :initial_value, :value, :check_value
|
||||
mutually_exclusive_parameters :initial_value, :value
|
||||
|
||||
def initialize(params = {}, env = nil)
|
||||
super(params, env)
|
||||
clear
|
||||
end
|
||||
|
||||
# Resets the internal state to that of a newly created object.
|
||||
def clear
|
||||
@value = nil
|
||||
@in_read = false
|
||||
end
|
||||
|
||||
# Returns if the value of this data has been read or explicitly set.
|
||||
def clear?
|
||||
@value.nil?
|
||||
end
|
||||
|
||||
# Single objects are single_values
|
||||
def single_value?
|
||||
true
|
||||
end
|
||||
|
||||
# To be called after calling #do_read.
|
||||
def done_read
|
||||
@in_read = false
|
||||
end
|
||||
|
||||
# Returns the current value of this data.
|
||||
def value
|
||||
_value
|
||||
end
|
||||
|
||||
# Sets the value of this data.
|
||||
def value=(v)
|
||||
# only allow modification if the value isn't predefined
|
||||
unless has_param?(:value)
|
||||
raise ArgumentError, "can't set a nil value" if v.nil?
|
||||
@value = v
|
||||
|
||||
# Note that this doesn't do anything in ruby 1.8.x so ignore for now
|
||||
# # explicitly return the output of #value as v may be different
|
||||
# self.value
|
||||
end
|
||||
end
|
||||
|
||||
#---------------
|
||||
private
|
||||
|
||||
# Reads the value for this data from +io+.
|
||||
def _do_read(io)
|
||||
@in_read = true
|
||||
@value = read_val(io)
|
||||
|
||||
# does the value meet expectations?
|
||||
if has_param?(:check_value)
|
||||
current_value = self.value
|
||||
expected = eval_param(:check_value, :value => current_value)
|
||||
if not expected
|
||||
raise ValidityError, "value '#{current_value}' not as expected"
|
||||
elsif current_value != expected and expected != true
|
||||
raise ValidityError, "value is '#{current_value}' but " +
|
||||
"expected '#{expected}'"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Writes the value for this data to +io+.
|
||||
def _do_write(io)
|
||||
raise "can't write whilst reading" if @in_read
|
||||
io.writebytes(val_to_str(_value))
|
||||
end
|
||||
|
||||
# Returns the number of bytes it will take to write this data.
|
||||
def _do_num_bytes(ignored)
|
||||
val_to_str(_value).length
|
||||
end
|
||||
|
||||
# Returns a snapshot of this data object.
|
||||
def _snapshot
|
||||
value
|
||||
end
|
||||
|
||||
# The unmodified value of this data object. Note that #value calls this
|
||||
# method. This is so that #value can be overridden in subclasses to
|
||||
# modify the value.
|
||||
def _value
|
||||
# Table of possible preconditions and expected outcome
|
||||
# 1. :value and !in_read -> :value
|
||||
# 2. :value and in_read -> @value
|
||||
# 3. :initial_value and clear? -> :initial_value
|
||||
# 4. :initial_value and !clear? -> @value
|
||||
# 5. clear? -> sensible_default
|
||||
# 6. !clear? -> @value
|
||||
|
||||
if not @in_read and (evaluated_value = eval_param(:value))
|
||||
# rule 1 above
|
||||
evaluated_value
|
||||
else
|
||||
# combining all other rules gives this simplified expression
|
||||
@value || eval_param(:value) ||
|
||||
eval_param(:initial_value) || sensible_default()
|
||||
end
|
||||
end
|
||||
|
||||
###########################################################################
|
||||
# To be implemented by subclasses
|
||||
|
||||
# Return the string representation that +val+ will take when written.
|
||||
def val_to_str(val)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Read a number of bytes from +io+ and return the value they represent.
|
||||
def read_val(io)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Return a sensible default for this data.
|
||||
def sensible_default
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# To be implemented by subclasses
|
||||
###########################################################################
|
||||
end
|
||||
end
|
|
@ -0,0 +1,194 @@
|
|||
require 'bindata/single'
|
||||
require 'bindata/struct'
|
||||
|
||||
module BinData
|
||||
# A SingleValue is a declarative way to define a new BinData data type.
|
||||
# The data type must contain a single value only. For new data types
|
||||
# that contain multiple values see BinData::MultiValue.
|
||||
#
|
||||
# To define a new data type, set fields as if for MultiValue and add a
|
||||
# #get and #set method to extract / convert the data between the fields
|
||||
# and the #value of the object.
|
||||
#
|
||||
# require 'bindata'
|
||||
#
|
||||
# class PascalString < BinData::SingleValue
|
||||
# uint8 :len, :value => lambda { data.length }
|
||||
# string :data, :read_length => :len
|
||||
#
|
||||
# def get
|
||||
# self.data
|
||||
# end
|
||||
#
|
||||
# def set(v)
|
||||
# self.data = v
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# ps = PascalString.new(:initial_value => "hello")
|
||||
# ps.to_s #=> "\005hello"
|
||||
# ps.read("\003abcde")
|
||||
# ps.value #=> "abc"
|
||||
#
|
||||
# # Unsigned 24 bit big endian integer
|
||||
# class Uint24be < BinData::SingleValue
|
||||
# uint8 :byte1
|
||||
# uint8 :byte2
|
||||
# uint8 :byte3
|
||||
#
|
||||
# def get
|
||||
# (self.byte1 << 16) | (self.byte2 << 8) | self.byte3
|
||||
# end
|
||||
#
|
||||
# def set(v)
|
||||
# v = 0 if v < 0
|
||||
# v = 0xffffff if v > 0xffffff
|
||||
#
|
||||
# self.byte1 = (v >> 16) & 0xff
|
||||
# self.byte2 = (v >> 8) & 0xff
|
||||
# self.byte3 = v & 0xff
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# u24 = Uint24be.new
|
||||
# u24.read("\x12\x34\x56")
|
||||
# "0x%x" % u24.value #=> 0x123456
|
||||
#
|
||||
# == Parameters
|
||||
#
|
||||
# SingleValue objects accept all the parameters that BinData::Single do.
|
||||
#
|
||||
class SingleValue < Single
|
||||
|
||||
class << self
|
||||
|
||||
# Register the names of all subclasses of this class.
|
||||
def inherited(subclass) #:nodoc:
|
||||
register(subclass.name, subclass)
|
||||
end
|
||||
|
||||
# Returns or sets the endianess of numerics used in this stucture.
|
||||
# Endianess is applied to the fields of this structure.
|
||||
# Valid values are :little and :big.
|
||||
def endian(endian = nil)
|
||||
@endian ||= nil
|
||||
if [:little, :big].include?(endian)
|
||||
@endian = endian
|
||||
elsif endian != nil
|
||||
raise ArgumentError, "unknown value for endian '#{endian}'"
|
||||
end
|
||||
@endian
|
||||
end
|
||||
|
||||
# Returns all stored fields. Should only be called by
|
||||
# #sanitize_parameters
|
||||
def fields
|
||||
@fields || []
|
||||
end
|
||||
|
||||
# Used to define fields for the internal structure.
|
||||
def method_missing(symbol, *args)
|
||||
name, params = args
|
||||
|
||||
type = symbol
|
||||
name = (name.nil? or name == "") ? nil : name.to_s
|
||||
params ||= {}
|
||||
|
||||
# note that fields are stored in an instance variable not a class var
|
||||
@fields ||= []
|
||||
|
||||
# check that type is known
|
||||
unless Sanitizer.type_exists?(type, endian)
|
||||
raise TypeError, "unknown type '#{type}' for #{self}", caller
|
||||
end
|
||||
|
||||
# check that name is okay
|
||||
if name != nil
|
||||
# check for duplicate names
|
||||
@fields.each do |t, n, p|
|
||||
if n == name
|
||||
raise SyntaxError, "duplicate field '#{name}' in #{self}", caller
|
||||
end
|
||||
end
|
||||
|
||||
# check that name doesn't shadow an existing method
|
||||
if self.instance_methods.include?(name)
|
||||
raise NameError.new("", name),
|
||||
"field '#{name}' shadows an existing method", caller
|
||||
end
|
||||
end
|
||||
|
||||
# remember this field. These fields will be recalled upon creating
|
||||
# an instance of this class
|
||||
@fields.push([type, name, params])
|
||||
end
|
||||
|
||||
# Ensures that +params+ is of the form expected by #initialize.
|
||||
def sanitize_parameters!(sanitizer, params)
|
||||
struct_params = {}
|
||||
struct_params[:fields] = self.fields
|
||||
struct_params[:endian] = self.endian unless self.endian.nil?
|
||||
|
||||
params[:struct_params] = struct_params
|
||||
|
||||
super(sanitizer, params)
|
||||
end
|
||||
end
|
||||
|
||||
# These are the parameters used by this class.
|
||||
mandatory_parameter :struct_params
|
||||
|
||||
def initialize(params = {}, env = nil)
|
||||
super(params, env)
|
||||
|
||||
@struct = BinData::Struct.new(param(:struct_params), create_env)
|
||||
end
|
||||
|
||||
# Forward method calls to the internal struct.
|
||||
def method_missing(symbol, *args, &block)
|
||||
if @struct.respond_to?(symbol)
|
||||
@struct.__send__(symbol, *args, &block)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
#---------------
|
||||
private
|
||||
|
||||
# Retrieve a sensible default from the internal struct.
|
||||
def sensible_default
|
||||
get
|
||||
end
|
||||
|
||||
# Read data into the fields of the internal struct then return the value.
|
||||
def read_val(io)
|
||||
@struct.read(io)
|
||||
get
|
||||
end
|
||||
|
||||
# Sets +val+ into the fields of the internal struct then returns the
|
||||
# string representation.
|
||||
def val_to_str(val)
|
||||
set(val)
|
||||
@struct.to_s
|
||||
end
|
||||
|
||||
###########################################################################
|
||||
# To be implemented by subclasses
|
||||
|
||||
# Extracts the value for this data object from the fields of the
|
||||
# internal struct.
|
||||
def get
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Sets the fields of the internal struct to represent +v+.
|
||||
def set(v)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# To be implemented by subclasses
|
||||
###########################################################################
|
||||
end
|
||||
end
|
|
@ -0,0 +1,115 @@
|
|||
require "bindata/single"
|
||||
|
||||
module BinData
|
||||
# A String is a sequence of bytes. This is the same as strings in Ruby.
|
||||
# The issue of character encoding is ignored by this class.
|
||||
#
|
||||
# require 'bindata'
|
||||
#
|
||||
# data = "abcdefghij"
|
||||
#
|
||||
# obj = BinData::String.new(:read_length => 5)
|
||||
# obj.read(data)
|
||||
# obj.value #=> "abcde"
|
||||
#
|
||||
# obj = BinData::String.new(:length => 6)
|
||||
# obj.read(data)
|
||||
# obj.value #=> "abcdef"
|
||||
# obj.value = "abcdefghij"
|
||||
# obj.value #=> "abcdef"
|
||||
# obj.value = "abcd"
|
||||
# obj.value #=> "abcd\000\000"
|
||||
#
|
||||
# obj = BinData::String.new(:length => 6, :trim_value => true)
|
||||
# obj.value = "abcd"
|
||||
# obj.value #=> "abcd"
|
||||
# obj.to_s #=> "abcd\000\000"
|
||||
#
|
||||
# obj = BinData::String.new(:length => 6, :pad_char => 'A')
|
||||
# obj.value = "abcd"
|
||||
# obj.value #=> "abcdAA"
|
||||
# obj.to_s #=> "abcdAA"
|
||||
#
|
||||
# == Parameters
|
||||
#
|
||||
# String objects accept all the params that BinData::Single
|
||||
# does, as well as the following:
|
||||
#
|
||||
# <tt>:read_length</tt>:: The length to use when reading a value.
|
||||
# <tt>:length</tt>:: The fixed length of the string. If a shorter
|
||||
# string is set, it will be padded to this length.
|
||||
# <tt>:pad_char</tt>:: The character to use when padding a string to a
|
||||
# set length. Valid values are Integers and
|
||||
# Strings of length 1. "\0" is the default.
|
||||
# <tt>:trim_value</tt>:: Boolean, default false. If set, #value will
|
||||
# return the value with all pad_chars trimmed
|
||||
# from the end of the string. The value will
|
||||
# not be trimmed when writing.
|
||||
class String < BinData::Single
|
||||
|
||||
# Register this class
|
||||
register(self.name, self)
|
||||
|
||||
# These are the parameters used by this class.
|
||||
optional_parameters :read_length, :length, :trim_value
|
||||
default_parameters :pad_char => "\0"
|
||||
mutually_exclusive_parameters :read_length, :length
|
||||
mutually_exclusive_parameters :length, :value
|
||||
|
||||
class << self
|
||||
|
||||
# Ensures that +params+ is of the form expected by #initialize.
|
||||
def sanitize_parameters!(sanitizer, params)
|
||||
# warn about deprecated param - remove before releasing 1.0
|
||||
if params[:initial_length]
|
||||
warn ":initial_length is deprecated. Replacing with :read_length"
|
||||
params[:read_length] = params.delete(:initial_length)
|
||||
end
|
||||
|
||||
# set :pad_char to be a single length character string
|
||||
if params.has_key?(:pad_char)
|
||||
ch = params[:pad_char]
|
||||
ch = ch.respond_to?(:chr) ? ch.chr : ch.to_s
|
||||
if ch.length > 1
|
||||
raise ArgumentError, ":pad_char must not contain more than 1 char"
|
||||
end
|
||||
params[:pad_char] = ch
|
||||
end
|
||||
|
||||
super(sanitizer, params)
|
||||
end
|
||||
end
|
||||
|
||||
# Overrides value to return the value padded to the desired length or
|
||||
# trimmed as required.
|
||||
def value
|
||||
v = val_to_str(_value)
|
||||
v.sub!(/#{eval_param(:pad_char)}*$/, "") if param(:trim_value) == true
|
||||
v
|
||||
end
|
||||
|
||||
#---------------
|
||||
private
|
||||
|
||||
# Returns +val+ ensuring that it is padded to the desired length.
|
||||
def val_to_str(val)
|
||||
# trim val if necessary
|
||||
len = eval_param(:length) || val.length
|
||||
str = val.slice(0, len)
|
||||
|
||||
# then pad to length if str is short
|
||||
str << (eval_param(:pad_char) * (len - str.length))
|
||||
end
|
||||
|
||||
# Read a number of bytes from +io+ and return the value they represent.
|
||||
def read_val(io)
|
||||
len = eval_param(:read_length) || eval_param(:length) || 0
|
||||
io.readbytes(len)
|
||||
end
|
||||
|
||||
# Returns an empty string as default.
|
||||
def sensible_default
|
||||
""
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,98 @@
|
|||
require "bindata/single"
|
||||
|
||||
module BinData
|
||||
# A BinData::Stringz object is a container for a zero ("\0") terminated
|
||||
# string.
|
||||
#
|
||||
# For convenience, the zero terminator is not necessary when setting the
|
||||
# value. Likewise, the returned value will not be zero terminated.
|
||||
#
|
||||
# require 'bindata'
|
||||
#
|
||||
# data = "abcd\x00efgh"
|
||||
#
|
||||
# obj = BinData::Stringz.new
|
||||
# obj.read(data)
|
||||
# obj.snapshot #=> "abcd"
|
||||
# obj.value #=> "abcd"
|
||||
# obj.num_bytes #=> 5
|
||||
# obj.to_s #=> "abcd\000"
|
||||
#
|
||||
# == Parameters
|
||||
#
|
||||
# Stringz objects accept all the params that BinData::Single
|
||||
# does, as well as the following:
|
||||
#
|
||||
# <tt>:max_length</tt>:: The maximum length of the string including the zero
|
||||
# byte.
|
||||
class Stringz < BinData::Single
|
||||
|
||||
# Register this class
|
||||
register(self.name, self)
|
||||
|
||||
# These are the parameters used by this class.
|
||||
optional_parameters :max_length
|
||||
|
||||
# Overrides value to return the value of this data excluding the trailing
|
||||
# zero byte.
|
||||
def value
|
||||
v = super
|
||||
val_to_str(v).chomp("\0")
|
||||
end
|
||||
|
||||
#---------------
|
||||
private
|
||||
|
||||
# Returns +val+ ensuring it is zero terminated and no longer
|
||||
# than <tt>:max_length</tt> bytes.
|
||||
def val_to_str(val)
|
||||
zero_terminate(val, eval_param(:max_length))
|
||||
end
|
||||
|
||||
# Read a number of bytes from +io+ and return the value they represent.
|
||||
def read_val(io)
|
||||
max_length = eval_param(:max_length)
|
||||
str = ""
|
||||
i = 0
|
||||
ch = nil
|
||||
|
||||
# read until zero byte or we have read in the max number of bytes
|
||||
while ch != "\0" and i != max_length
|
||||
ch = io.readbytes(1)
|
||||
str << ch
|
||||
i += 1
|
||||
end
|
||||
|
||||
zero_terminate(str, max_length)
|
||||
end
|
||||
|
||||
# Returns an empty string as default.
|
||||
def sensible_default
|
||||
""
|
||||
end
|
||||
|
||||
# Returns +str+ after it has been zero terminated. The returned string
|
||||
# will not be longer than +max_length+.
|
||||
def zero_terminate(str, max_length = nil)
|
||||
# str must not be empty
|
||||
result = (str == "") ? "\0" : str
|
||||
|
||||
# remove anything after the first \0
|
||||
result = result.sub(/([^\0]*\0).*/, '\1')
|
||||
|
||||
# trim string to be no longer than max_length including zero byte
|
||||
if max_length
|
||||
max_length = 1 if max_length < 1
|
||||
result = result[0, max_length]
|
||||
if result.length == max_length and result[-1, 1] != "\0"
|
||||
result[-1, 1] = "\0"
|
||||
end
|
||||
end
|
||||
|
||||
# ensure last byte in the string is a zero byte
|
||||
result << "\0" if result[-1, 1] != "\0"
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,383 @@
|
|||
require 'bindata/base'
|
||||
require 'bindata/sanitize'
|
||||
|
||||
module BinData
|
||||
# A Struct is an ordered collection of named data objects.
|
||||
#
|
||||
# require 'bindata'
|
||||
#
|
||||
# class Tuple < BinData::MultiValue
|
||||
# int8 :x
|
||||
# int8 :y
|
||||
# int8 :z
|
||||
# end
|
||||
#
|
||||
# obj = BinData::Struct.new(:hide => :a,
|
||||
# :fields => [ [:int32le, :a],
|
||||
# [:int16le, :b],
|
||||
# [:tuple, :s] ])
|
||||
# obj.field_names =># ["b", "s"]
|
||||
#
|
||||
#
|
||||
# == Parameters
|
||||
#
|
||||
# Parameters may be provided at initialisation to control the behaviour of
|
||||
# an object. These params are:
|
||||
#
|
||||
# <tt>:fields</tt>:: An array specifying the fields for this struct.
|
||||
# Each element of the array is of the form [type, name,
|
||||
# params]. Type is a symbol representing a registered
|
||||
# type. Name is the name of this field. Params is an
|
||||
# optional hash of parameters to pass to this field
|
||||
# when instantiating it.
|
||||
# <tt>:hide</tt>:: A list of the names of fields that are to be hidden
|
||||
# from the outside world. Hidden fields don't appear
|
||||
# in #snapshot or #field_names but are still accessible
|
||||
# by name.
|
||||
# <tt>:endian</tt>:: Either :little or :big. This specifies the default
|
||||
# endian of any numerics in this struct, or in any
|
||||
# nested data objects.
|
||||
class Struct < BinData::Base
|
||||
|
||||
# These reserved words may not be used as field names
|
||||
RESERVED = (::Hash.instance_methods +
|
||||
%w{alias and begin break case class def defined do else elsif
|
||||
end ensure false for if in module next nil not or redo
|
||||
rescue retry return self super then true undef unless until
|
||||
when while yield }).uniq
|
||||
|
||||
# Register this class
|
||||
register(self.name, self)
|
||||
|
||||
# A hash that can be accessed via attributes.
|
||||
class Snapshot < Hash #:nodoc:
|
||||
def method_missing(symbol, *args)
|
||||
self[symbol.id2name] || super
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
#### DEPRECATION HACK to allow inheriting from BinData::Struct
|
||||
#
|
||||
def inherited(subclass) #:nodoc:
|
||||
if subclass != MultiValue
|
||||
# warn about deprecated method - remove before releasing 1.0
|
||||
warn "warning: inheriting from BinData::Struct in deprecated. Inherit from BinData::MultiValue instead."
|
||||
|
||||
register(subclass.name, subclass)
|
||||
end
|
||||
end
|
||||
def endian(endian = nil)
|
||||
@endian ||= nil
|
||||
if [:little, :big].include?(endian)
|
||||
@endian = endian
|
||||
elsif endian != nil
|
||||
raise ArgumentError, "unknown value for endian '#{endian}'"
|
||||
end
|
||||
@endian
|
||||
end
|
||||
def hide(*args)
|
||||
# note that fields are stored in an instance variable not a class var
|
||||
@hide ||= []
|
||||
args.each do |name|
|
||||
@hide << name.to_s
|
||||
end
|
||||
@hide
|
||||
end
|
||||
def fields
|
||||
@fields || []
|
||||
end
|
||||
def method_missing(symbol, *args)
|
||||
name, params = args
|
||||
|
||||
type = symbol
|
||||
name = name.to_s
|
||||
params ||= {}
|
||||
|
||||
# note that fields are stored in an instance variable not a class var
|
||||
@fields ||= []
|
||||
|
||||
# check that type is known
|
||||
unless Sanitizer.type_exists?(type, endian)
|
||||
raise TypeError, "unknown type '#{type}' for #{self}", caller
|
||||
end
|
||||
|
||||
# check for duplicate names
|
||||
@fields.each do |t, n, p|
|
||||
if n == name
|
||||
raise SyntaxError, "duplicate field '#{name}' in #{self}", caller
|
||||
end
|
||||
end
|
||||
|
||||
# check that name doesn't shadow an existing method
|
||||
if self.instance_methods.include?(name)
|
||||
raise NameError.new("", name),
|
||||
"field '#{name}' shadows an existing method", caller
|
||||
end
|
||||
|
||||
# check that name isn't reserved
|
||||
if self::RESERVED.include?(name)
|
||||
raise NameError.new("", name),
|
||||
"field '#{name}' is a reserved name", caller
|
||||
end
|
||||
|
||||
# remember this field. These fields will be recalled upon creating
|
||||
# an instance of this class
|
||||
@fields.push([type, name, params])
|
||||
end
|
||||
def deprecated_hack!(params)
|
||||
# possibly override endian
|
||||
endian = params[:endian] || self.endian
|
||||
params[:endian] = endian unless endian.nil?
|
||||
params[:fields] = params[:fields] || self.fields
|
||||
params[:hide] = params[:hide] || self.hide
|
||||
end
|
||||
#
|
||||
#### DEPRECATION HACK to allow inheriting from BinData::Struct
|
||||
|
||||
|
||||
# Ensures that +params+ is of the form expected by #initialize.
|
||||
def sanitize_parameters!(sanitizer, params)
|
||||
#### DEPRECATION HACK to allow inheriting from BinData::Struct
|
||||
#
|
||||
deprecated_hack!(params)
|
||||
#
|
||||
#### DEPRECATION HACK to allow inheriting from BinData::Struct
|
||||
|
||||
# possibly override endian
|
||||
endian = params[:endian]
|
||||
if endian != nil
|
||||
unless [:little, :big].include?(endian)
|
||||
raise ArgumentError, "unknown value for endian '#{endian}'"
|
||||
end
|
||||
|
||||
params[:endian] = endian
|
||||
end
|
||||
|
||||
if params.has_key?(:fields)
|
||||
sanitizer.with_endian(endian) do
|
||||
# ensure names of fields are strings and that params is sanitized
|
||||
all_fields = params[:fields].collect do |ftype, fname, fparams|
|
||||
fname = fname.to_s
|
||||
klass, sanitized_fparams = sanitizer.sanitize(ftype, fparams)
|
||||
[klass, fname, sanitized_fparams]
|
||||
end
|
||||
params[:fields] = all_fields
|
||||
end
|
||||
|
||||
# now params are sanitized, check that parameter names are okay
|
||||
field_names = []
|
||||
instance_methods = self.instance_methods
|
||||
reserved_names = RESERVED
|
||||
|
||||
params[:fields].each do |fklass, fname, fparams|
|
||||
|
||||
# check that name doesn't shadow an existing method
|
||||
if instance_methods.include?(fname)
|
||||
raise NameError.new("Rename field '#{fname}' in #{self}, " +
|
||||
"as it shadows an existing method.", fname)
|
||||
end
|
||||
|
||||
# check that name isn't reserved
|
||||
if reserved_names.include?(fname)
|
||||
raise NameError.new("Rename field '#{fname}' in #{self}, " +
|
||||
"as it is a reserved name.", fname)
|
||||
end
|
||||
|
||||
# check for multiple definitions
|
||||
if field_names.include?(fname)
|
||||
raise NameError.new("field '#{fname}' in #{self}, " +
|
||||
"is defined multiple times.", fname)
|
||||
end
|
||||
|
||||
field_names << fname
|
||||
end
|
||||
|
||||
# collect all hidden names that correspond to a field name
|
||||
hide = []
|
||||
if params.has_key?(:hide)
|
||||
hidden = (params[:hide] || []).collect { |h| h.to_s }
|
||||
all_field_names = params[:fields].collect { |k,n,p| n }
|
||||
hide = hidden & all_field_names
|
||||
end
|
||||
params[:hide] = hide
|
||||
end
|
||||
|
||||
super(sanitizer, params)
|
||||
end
|
||||
end
|
||||
|
||||
# These are the parameters used by this class.
|
||||
mandatory_parameter :fields
|
||||
optional_parameters :endian, :hide
|
||||
|
||||
# Creates a new Struct.
|
||||
def initialize(params = {}, env = nil)
|
||||
super(params, env)
|
||||
|
||||
# extract field names but don't instantiate the fields
|
||||
@field_names = param(:fields).collect { |k, n, p| n }
|
||||
@field_objs = []
|
||||
end
|
||||
|
||||
# Clears the field represented by +name+. If no +name+
|
||||
# is given, clears all fields in the struct.
|
||||
def clear(name = nil)
|
||||
if name.nil?
|
||||
@field_objs.each { |f| f.clear unless f.nil? }
|
||||
else
|
||||
obj = find_obj_for_name(name.to_s)
|
||||
obj.clear unless obj.nil?
|
||||
end
|
||||
end
|
||||
|
||||
# Returns if the field represented by +name+ is clear?. If no +name+
|
||||
# is given, returns whether all fields are clear.
|
||||
def clear?(name = nil)
|
||||
if name.nil?
|
||||
@field_objs.each do |f|
|
||||
return false unless f.nil? or f.clear?
|
||||
end
|
||||
true
|
||||
else
|
||||
obj = find_obj_for_name(name.to_s)
|
||||
obj.nil? ? true : obj.clear?
|
||||
end
|
||||
end
|
||||
|
||||
# Returns whether this data object contains a single value. Single
|
||||
# value data objects respond to <tt>#value</tt> and <tt>#value=</tt>.
|
||||
def single_value?
|
||||
return false
|
||||
end
|
||||
|
||||
# Returns a list of the names of all fields accessible through this
|
||||
# object. +include_hidden+ specifies whether to include hidden names
|
||||
# in the listing.
|
||||
def field_names(include_hidden = false)
|
||||
# collect field names
|
||||
names = []
|
||||
hidden = param(:hide)
|
||||
@field_names.each do |name|
|
||||
if include_hidden or not hidden.include?(name)
|
||||
names << name
|
||||
end
|
||||
end
|
||||
names
|
||||
end
|
||||
|
||||
# To be called after calling #read.
|
||||
def done_read
|
||||
@field_objs.each { |f| f.done_read unless f.nil? }
|
||||
end
|
||||
|
||||
# Returns the data object that stores values for +name+.
|
||||
def find_obj_for_name(name)
|
||||
idx = @field_names.index(name)
|
||||
if idx
|
||||
instantiate_obj(idx)
|
||||
@field_objs[idx]
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def offset_of(field)
|
||||
idx = @field_names.index(field.to_s)
|
||||
if idx
|
||||
instantiate_all
|
||||
|
||||
offset = 0
|
||||
(0...idx).each do |i|
|
||||
this_offset = @field_objs[i].do_num_bytes
|
||||
if ::Float === offset and ::Integer === this_offset
|
||||
offset = offset.ceil
|
||||
end
|
||||
offset += this_offset
|
||||
end
|
||||
offset
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Override to include field names
|
||||
alias_method :orig_respond_to?, :respond_to?
|
||||
def respond_to?(symbol, include_private = false)
|
||||
orig_respond_to?(symbol, include_private) ||
|
||||
field_names(true).include?(symbol.id2name.chomp("="))
|
||||
end
|
||||
|
||||
def method_missing(symbol, *args, &block)
|
||||
name = symbol.id2name
|
||||
|
||||
is_writer = (name[-1, 1] == "=")
|
||||
name.chomp!("=")
|
||||
|
||||
# find the object that is responsible for name
|
||||
if (obj = find_obj_for_name(name))
|
||||
# pass on the request
|
||||
if obj.single_value? and is_writer
|
||||
obj.value = *args
|
||||
elsif obj.single_value?
|
||||
obj.value
|
||||
else
|
||||
obj
|
||||
end
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
#---------------
|
||||
private
|
||||
|
||||
# Instantiates all fields.
|
||||
def instantiate_all
|
||||
(0...@field_names.length).each { |idx| instantiate_obj(idx) }
|
||||
end
|
||||
|
||||
# Instantiates the field object at position +idx+.
|
||||
def instantiate_obj(idx)
|
||||
if @field_objs[idx].nil?
|
||||
fklass, fname, fparams = param(:fields)[idx]
|
||||
@field_objs[idx] = fklass.new(fparams, create_env)
|
||||
end
|
||||
end
|
||||
|
||||
# Reads the values for all fields in this object from +io+.
|
||||
def _do_read(io)
|
||||
instantiate_all
|
||||
@field_objs.each { |f| f.do_read(io) }
|
||||
end
|
||||
|
||||
# Writes the values for all fields in this object to +io+.
|
||||
def _do_write(io)
|
||||
instantiate_all
|
||||
@field_objs.each { |f| f.do_write(io) }
|
||||
end
|
||||
|
||||
# Returns the number of bytes it will take to write the field represented
|
||||
# by +name+. If +name+ is nil then returns the number of bytes required
|
||||
# to write all fields.
|
||||
def _do_num_bytes(name)
|
||||
if name.nil?
|
||||
instantiate_all
|
||||
(@field_objs.inject(0) { |sum, f| sum + f.do_num_bytes }).ceil
|
||||
else
|
||||
obj = find_obj_for_name(name.to_s)
|
||||
obj.nil? ? 0 : obj.do_num_bytes
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a snapshot of this struct as a hash.
|
||||
def _snapshot
|
||||
hash = Snapshot.new
|
||||
field_names.each do |name|
|
||||
ss = find_obj_for_name(name).snapshot
|
||||
hash[name] = ss unless ss.nil?
|
||||
end
|
||||
hash
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,70 @@
|
|||
if VERSION < "1.8.6"
|
||||
raise RuntimeError, "Ruby not at a minimum version of 1.8.6"
|
||||
end
|
||||
|
||||
require 'bindata'
|
||||
|
||||
# This version requirement is a bit of a lie; we need svn version
|
||||
# 99 or later, so we can make use of this commit:
|
||||
# r99 | dmendel | 2008-07-24 23:45:49 -0500 (Thu, 24 Jul 2008) | 1 line
|
||||
#
|
||||
# Allow arrays to read until eof
|
||||
#
|
||||
# So, for now, PacketFu will distribute with a slightly forked BinData.
|
||||
# We'll unfork when 0.9.3 is released and all will be right with the world.
|
||||
if BinData::VERSION < "0.9.2-eofpatch"
|
||||
raise LoadError, "BinData not at version 0.9.2-eofpatch"
|
||||
end
|
||||
|
||||
require 'ipaddr'
|
||||
require 'singleton'
|
||||
|
||||
module PacketFu
|
||||
@@pcaprub_loaded = false
|
||||
begin
|
||||
require 'pcaprub'
|
||||
if Pcap.version < "0.8-dev"
|
||||
@@pcaprub_loaded = false # Don't bother with broken versions
|
||||
raise LoadError, "PcapRub not at a minimum version of 0.8-dev"
|
||||
end
|
||||
require 'packetfu/capture'
|
||||
require 'packetfu/read'
|
||||
require 'packetfu/inject'
|
||||
rescue LoadError
|
||||
end
|
||||
end
|
||||
|
||||
# Doesn't require PcapRub
|
||||
require 'packetfu/pcap'
|
||||
require 'packetfu/write'
|
||||
|
||||
# Packet crafting/parsing goodness.
|
||||
require 'packetfu/packet'
|
||||
require 'packetfu/invalid'
|
||||
require 'packetfu/eth'
|
||||
require 'packetfu/ip'
|
||||
require 'packetfu/arp'
|
||||
require 'packetfu/icmp'
|
||||
require 'packetfu/udp'
|
||||
require 'packetfu/tcp'
|
||||
require 'packetfu/ipv6'
|
||||
|
||||
# Various often-used utilities.
|
||||
require 'packetfu/utils'
|
||||
|
||||
# A place to keep defaults.
|
||||
require 'packetfu/config'
|
||||
|
||||
#:main:PacketFu
|
||||
#
|
||||
#:include:../README
|
||||
#:include:../LICENSE
|
||||
|
||||
module PacketFu
|
||||
|
||||
# Returns the version.
|
||||
def self.version
|
||||
"0.1.0" # September 13, 2008
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,26 @@
|
|||
Copyright (c) 2008, Tod Beardsley
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of Tod Beardsley nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY TOD BEARDSLEY ''AS IS'' AND ANY
|
||||
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL TOD BEARDSLEY BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
= PacketFu
|
||||
|
||||
A library for reading a writing packets to an interface or to a libpcap-formatted file.
|
||||
It is maintained at http://code.google.com/p/packetfu
|
||||
|
||||
== Installation
|
||||
|
||||
PacketFu should live somewhere in your path. I haven't decided yet on packaging, will probably succumb to gems; your best bet is to just drop it into /usr/local/lib/site_ruby/1.8 or where ever you install bindata to.
|
||||
|
||||
== Requirements
|
||||
|
||||
BinData: http://bindata.rubyforge.org
|
||||
|
||||
Dion Mendel's BinData is absolutely critical for PacketFu. Specifically, BinData's subversion r99 or later is required, in order to make use of the :eof patch. So, BinData r101 is included in this distribution until 0.9.3 or later is released.
|
||||
|
||||
PcapRub: www.metasploit.com/svn/framework3/trunk/external/pcaprub
|
||||
|
||||
Marshall Beddoe's PcapRub is required only for packet reading and writing from a network interfaces (which is a pretty big only). PcapRub itself relies on libpcap 0.9.8 or later for packet injection. It also requires root privilieges to access the interface directly. Perhaps most noteworthy, PcapRub is <b>not</b> included in this distribution, as the vagaries of libpcap driver install can lead to some unexpected results. You are on your own for that.
|
||||
|
||||
== Examples
|
||||
|
||||
Wouldn't that be nice? The best way to learn right now is to pore over the documentation, and mess around with packetfu-shell.rb
|
||||
|
||||
== Author
|
||||
|
||||
PacketFu is maintained primarily by Tod Beardsley <todb@planb-security.net>
|
||||
|
||||
== License
|
|
@ -0,0 +1,177 @@
|
|||
|
||||
module PacketFu
|
||||
|
||||
# ARPHeader is a complete ARP struct, used in ARPPacket.
|
||||
#
|
||||
# ARP is used to discover the machine address of nearby devices.
|
||||
#
|
||||
# See http://www.networksorcery.com/enp/protocol/arp.htm for details.
|
||||
#
|
||||
# ==== Header Definition
|
||||
#
|
||||
# uint16be :arp_hw, :initial_value => 1 # Ethernet
|
||||
# uint16be :arp_proto, :initial_value => 0x0800 # IP
|
||||
# uint8 :arp_hw_len, :initial_value => 6
|
||||
# uint8 :arp_proto_len, :initial_value => 4
|
||||
# uint16be :arp_opcode, :initial_value => 1 # 1: Request, 2: Reply, 3: Request-Reverse, 4: Reply-Reverse
|
||||
# eth_mac :arp_src_mac # From eth.rb
|
||||
# octets :arp_src_ip # From ip.rb
|
||||
# eth_mac :arp_dst_mac # From eth.rb
|
||||
# octets :arp_dst_ip # From ip.rb
|
||||
# rest :body
|
||||
#
|
||||
class ARPHeader < BinData::MultiValue
|
||||
|
||||
uint16be :arp_hw, :initial_value => 1 # Ethernet
|
||||
uint16be :arp_proto, :initial_value => 0x0800 # IP
|
||||
uint8 :arp_hw_len, :initial_value => 6
|
||||
uint8 :arp_proto_len, :initial_value => 4
|
||||
uint16be :arp_opcode, :initial_value => 1 # 1: Request, 2: Reply, 3: Request-Reverse, 4: Reply-Reverse
|
||||
eth_mac :arp_src_mac # From eth.rb
|
||||
octets :arp_src_ip # From ip.rb
|
||||
eth_mac :arp_dst_mac # From eth.rb
|
||||
octets :arp_dst_ip # From ip.rb
|
||||
rest :body
|
||||
|
||||
# Set the source MAC address in a more readable way.
|
||||
def arp_saddr_mac=(mac)
|
||||
mac = EthHeader.mac2str(mac)
|
||||
self.arp_src_mac.read(mac)
|
||||
self.arp_src_mac
|
||||
end
|
||||
|
||||
# Returns a more readable source MAC address.
|
||||
def arp_saddr_mac
|
||||
EthHeader.str2mac(self.arp_src_mac.to_s)
|
||||
end
|
||||
|
||||
# Set the destination MAC address in a more readable way.
|
||||
def arp_daddr_mac=(mac)
|
||||
mac = EthHeader.mac2str(mac)
|
||||
self.arp_dst_mac.read(mac)
|
||||
self.arp_dst_mac
|
||||
end
|
||||
|
||||
# Returns a more readable source MAC address.
|
||||
def arp_daddr_mac
|
||||
EthHeader.str2mac(self.arp_dst_mac.to_s)
|
||||
end
|
||||
|
||||
# Sets a more readable source IP address.
|
||||
def arp_saddr_ip=(addr)
|
||||
addr = IPHeader.octet_array(addr)
|
||||
arp_src_ip.o1 = addr[0]
|
||||
arp_src_ip.o2 = addr[1]
|
||||
arp_src_ip.o3 = addr[2]
|
||||
arp_src_ip.o4 = addr[3]
|
||||
end
|
||||
|
||||
# Returns a more readable source IP address.
|
||||
def arp_saddr_ip
|
||||
[arp_src_ip.o1,arp_src_ip.o2,arp_src_ip.o3,arp_src_ip.o4].join('.')
|
||||
end
|
||||
|
||||
# Sets a more readable destination IP address.
|
||||
def arp_daddr_ip=(addr)
|
||||
addr = IPHeader.octet_array(addr)
|
||||
arp_dst_ip.o1 = addr[0]
|
||||
arp_dst_ip.o2 = addr[1]
|
||||
arp_dst_ip.o3 = addr[2]
|
||||
arp_dst_ip.o4 = addr[3]
|
||||
end
|
||||
|
||||
# Returns a more readable destination IP address.
|
||||
def arp_daddr_ip
|
||||
[arp_dst_ip.o1,arp_dst_ip.o2,arp_dst_ip.o3,arp_dst_ip.o4].join('.')
|
||||
end
|
||||
|
||||
end # class ARPHeader
|
||||
|
||||
# ARPPacket is used to construct ARP packets. They contain an EthHeader and an ARPHeader.
|
||||
# == Example
|
||||
#
|
||||
# require 'packetfu'
|
||||
# arp_pkt = PacketFu::ARPPacket.new(:flavor => "Windows")
|
||||
# arp_pkt.arp_saddr_mac="00:1c:23:44:55:66" # Your hardware address
|
||||
# arp_pkt.arp_saddr_ip="10.10.10.17" # Your IP address
|
||||
# arp_pkt.arp_daddr_ip="10.10.10.1" # Target IP address
|
||||
# arp_pkt.arp_opcode=1 # Request
|
||||
#
|
||||
# arp_pkt.to_w('eth0') # Inject on the wire. (requires root)
|
||||
# arp_pkt.to_f('/tmp/arp.pcap') # Write to a file.
|
||||
#
|
||||
# == Parameters
|
||||
#
|
||||
# :flavor
|
||||
# Sets the "flavor" of the ARP packet. Choices are currently:
|
||||
# :windows, :linux, :hp_deskjet
|
||||
# :eth
|
||||
# A pre-generated EthHeader object. If not specified, a new one will be created.
|
||||
# :arp
|
||||
# A pre-generated ARPHeader object. If not specificed, a new one will be created.
|
||||
# :config
|
||||
# A hash of return address details, often the output of Utils.whoami?
|
||||
class ARPPacket < Packet
|
||||
|
||||
attr_accessor :eth_header, :arp_header
|
||||
|
||||
def ethernet?; true; end
|
||||
def arp?; true; end
|
||||
|
||||
def initialize(args={})
|
||||
@eth_header = (args[:eth] || EthHeader.new)
|
||||
@arp_header = (args[:arp] || ARPHeader.new)
|
||||
@eth_header.eth_proto = 0x806
|
||||
@eth_header.body=@arp_header
|
||||
|
||||
# Please send more flavors to todb-packetfu@planb-security.net.
|
||||
# Most of these initial fingerprints come from one (1) sample.
|
||||
case (args[:flavor].nil?) ? :nil : args[:flavor].to_s.downcase.intern
|
||||
when :windows; @arp_header.body = "\x00" * 64 # 64 bytes of padding
|
||||
when :linux; @arp_header.body = "\x00" * 4 + # 32 bytes of padding
|
||||
"\x00\x07\x5c\x14" + "\x00" * 4 +
|
||||
"\x00\x0f\x83\x34" + "\x00\x0f\x83\x74" +
|
||||
"\x01\x11\x83\x78" + "\x00\x00\x00\x0c" +
|
||||
"\x00\x00\x00\x00"
|
||||
when :hp_deskjet; # Pads up to 60 bytes.
|
||||
@arp_header.body = "\xe0\x90\x0d\x6c" +
|
||||
"\xff\xff\xee\xee" + "\x00" * 4 +
|
||||
"\xe0\x8f\xfa\x18\x00\x20"
|
||||
else; @arp_header.body = "\x00" * 18 # Pads up to 60 bytes.
|
||||
end
|
||||
|
||||
@headers = [@eth_header, @arp_header]
|
||||
super
|
||||
|
||||
end
|
||||
|
||||
# Used to generate summary data for ARP packets.
|
||||
def peek(args={})
|
||||
peek_data = ["A "]
|
||||
peek_data << "%-5d" % self.to_s.size
|
||||
peek_data << self.arp_saddr_mac
|
||||
peek_data << "(#{self.arp_saddr_ip})"
|
||||
peek_data << "->"
|
||||
peek_data << case self.arp_daddr_mac
|
||||
when "00:00:00:00:00:00"; "Bcast00"
|
||||
when "ff:ff:ff:ff:ff:ff"; "BcastFF"
|
||||
else; self.arp_daddr_mac
|
||||
end
|
||||
peek_data << "(#{self.arp_daddr_ip})"
|
||||
peek_data << ":"
|
||||
peek_data << case self.arp_opcode
|
||||
when 1; "Requ"
|
||||
when 2; "Repl"
|
||||
when 3; "RReq"
|
||||
when 4; "RRpl"
|
||||
when 5; "IReq"
|
||||
when 6; "IRpl"
|
||||
else; "0x%02x" % self.opcode
|
||||
end
|
||||
peek_data.join
|
||||
end
|
||||
|
||||
end # class ARPPacket
|
||||
|
||||
end # module PacketFu
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
|
||||
module PacketFu
|
||||
|
||||
# The Capture class is used to construct PcapRub objects in order to collect
|
||||
# packets from an interface.
|
||||
#
|
||||
# This class requires PcapRub. In addition, you will need root (or root-like) privileges
|
||||
# in order to capture from the interface.
|
||||
#
|
||||
# Note, on some wireless cards, setting :promisc => true will disable capturing.
|
||||
#
|
||||
# == Example
|
||||
#
|
||||
# # Typical use
|
||||
# cap = PacketFu::Capture.new(:iface => 'eth0', :promisc => true)
|
||||
# cap.start
|
||||
# sleep 10
|
||||
# cap.save
|
||||
# first_packet = cap.array[0]
|
||||
#
|
||||
# # Tcpdump-like use
|
||||
# cap = PacketFu::Capture.new(:start => true)
|
||||
# cap.show_live(:save => true, :filter => 'tcp and not port 22')
|
||||
#
|
||||
# == See Also
|
||||
#
|
||||
# Read, Write
|
||||
class Capture
|
||||
attr_accessor :array, :stream # Leave these public and open.
|
||||
attr_reader :iface, :snaplen, :promisc, :timeout # Cant change after the init.
|
||||
|
||||
def initialize(args={})
|
||||
@array = [] # Where the packet array goes.
|
||||
@stream = [] # Where the stream goes.
|
||||
@iface = args[:iface] || Pcap.lookupdev
|
||||
@snaplen = args[:snaplen] || 0xffff
|
||||
@promisc = args[:promisc] || false # Sensible for some Intel wifi cards
|
||||
@timeout = args[:timeout] || 1
|
||||
|
||||
setup_params(args)
|
||||
end
|
||||
|
||||
def setup_params(args={})
|
||||
filter = args[:filter] # Not global; filter criteria can change.
|
||||
start = args[:start] || false
|
||||
capture if start
|
||||
bpf(:filter=>filter) if filter
|
||||
end
|
||||
|
||||
# capture() initializes the @stream varaible. Valid arguments are:
|
||||
#
|
||||
# :filter
|
||||
# Provide a bpf filter to enable for the capture. For example, 'ip and not tcp'
|
||||
# :start
|
||||
# When true, start capturing packets to the @stream variable. Defaults to true
|
||||
def capture(args={})
|
||||
if Process.euid.zero?
|
||||
filter = args[:filter]
|
||||
start = args[:start] || true
|
||||
if start
|
||||
begin
|
||||
@stream = Pcap.open_live(@iface,@snaplen,@promisc,@timeout)
|
||||
rescue RuntimeError
|
||||
$stderr.print "Are you sure you're root? Error: "
|
||||
raise
|
||||
end
|
||||
bpf(:filter=>filter) if filter
|
||||
else
|
||||
@stream = []
|
||||
end
|
||||
@stream
|
||||
else
|
||||
raise RuntimeError,"Not root, so can't capture packets. Error: "
|
||||
end
|
||||
end
|
||||
|
||||
# start() is equivalent to capture().
|
||||
def start(args={})
|
||||
capture(args)
|
||||
end
|
||||
|
||||
# clear() clears the @stream and @array variables, essentially starting the
|
||||
# capture session over. Valid arguments are:
|
||||
#
|
||||
# :array
|
||||
# If true, the @array is cleared.
|
||||
# :stream
|
||||
# If true, the @stream is cleared.
|
||||
def clear(args={})
|
||||
array = args[:array] || true
|
||||
stream = args[:stream] || true
|
||||
@array = [] if array
|
||||
@stream = [] if stream
|
||||
end
|
||||
|
||||
# bpf() sets a bpf filter on a capture session. Valid arugments are:
|
||||
#
|
||||
# :filter
|
||||
# Provide a bpf filter to enable for the capture. For example, 'ip and not tcp'
|
||||
def bpf(args={})
|
||||
filter = args[:filter]
|
||||
capture if @stream.class == Array
|
||||
@stream.setfilter(filter)
|
||||
end
|
||||
|
||||
# wire_to_array() saves a packet stream as an array of binary strings. From here,
|
||||
# packets may accessed by other functions. Note that the wire_to_array empties
|
||||
# the stream, so multiple calls will append new packets to @array.
|
||||
# Valid arguments are:
|
||||
#
|
||||
# :filter
|
||||
# Provide a bpf filter to apply to packets moving from @stream to @array.
|
||||
def wire_to_array(args={})
|
||||
filter = args[:filter]
|
||||
bpf(:filter=>filter) if filter
|
||||
|
||||
while this_pkt = @stream.next
|
||||
@array << this_pkt
|
||||
end
|
||||
@array.size
|
||||
end
|
||||
|
||||
# w2a() is a equivalent to wire_to_array()
|
||||
def w2a(args={})
|
||||
wire_to_array(args)
|
||||
end
|
||||
|
||||
# save() is a equivalent to wire_to_array()
|
||||
def save(args={})
|
||||
wire_to_array(args)
|
||||
end
|
||||
|
||||
# show_live() is a method to capture packets and display peek() data to stdout. Valid arguments are:
|
||||
#
|
||||
# :filter
|
||||
# Provide a bpf filter to captured packets.
|
||||
# :save
|
||||
# Save the capture in @array
|
||||
# :verbose
|
||||
# TODO: Not implemented yet; do more than just peek() at the packets.
|
||||
# :quiet
|
||||
# TODO: Not implemented yet; do less than peek() at the packets.
|
||||
def show_live(args={})
|
||||
filter = args[:filter]
|
||||
save = args[:save]
|
||||
verbose = args[:verbose] || args[:v] || false
|
||||
quiet = args[:quiet] || args[:q] || false # Setting q and v doesn't make a lot of sense but hey.
|
||||
|
||||
# Ensure the capture's started.
|
||||
if @stream.class == Array
|
||||
capture
|
||||
end
|
||||
|
||||
@stream.setfilter(filter) if filter
|
||||
while true
|
||||
@stream.each do |pkt|
|
||||
puts Packet.parse(pkt).peek
|
||||
@array << pkt if args[:save]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end # class Capture
|
||||
end # module PacketFu
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
|
||||
module PacketFu
|
||||
|
||||
# The Config class holds various bits of useful default information for packet creation.
|
||||
# If initialized without arguments, the @iface and @pcapfile instance variables are
|
||||
# set to the (pcaprub-believed) default interface and "/tmp/out.pcap", respectively.
|
||||
#
|
||||
# Any number of instance variables can be passed in to the intialize function (as a
|
||||
# hash), though only the expected network-related variables will be readable and
|
||||
# writeable directly.
|
||||
#
|
||||
# == Examples
|
||||
#
|
||||
# PacketFu::Config.new(:ip_saddr => "1.2.3.4").ip_saddr #=> "1.2.3.4"
|
||||
# PacketFu::Config.new(:foo=>"bar").foo #=> NomethodError: undefined method `foo'...
|
||||
#
|
||||
# The config() function, however, does provide access to custom variables:
|
||||
#
|
||||
# PacketFu::Config.new(:foo=>"bar").config[:foo] #=> "bar"
|
||||
# obj = PacketFu::Config.new(:foo=>"bar")
|
||||
# obj.config(:baz => "bat")
|
||||
# obj.config #=> {:iface=>"eth0", :baz=>"bat", :pcapfile=>"/tmp/out.pcap", :foo=>"bar"}
|
||||
class Config
|
||||
attr_accessor :eth_saddr, # The discovered eth_saddr
|
||||
:eth_daddr, # The discovered eth_daddr (ie, the gateway)
|
||||
:eth_src, # The discovered eth_src in binary form.
|
||||
:eth_dst, # The discovered eth_dst (gateway) in binary form.
|
||||
:ip_saddr, # The discovered ip_saddr
|
||||
:ip_src, # The discovered ip_src in binary form.
|
||||
:iface, # The declared interface.
|
||||
:pcapfile # A declared default file to write to.
|
||||
|
||||
def initialize(args={})
|
||||
if Process.euid.zero?
|
||||
@iface = Pcap.lookupdev || "lo" # In case there aren't any...
|
||||
end
|
||||
@pcapfile = "/tmp/out.pcap"
|
||||
args.each_pair { |k,v| self.instance_variable_set(("@" + k.to_s).intern,v) }
|
||||
end
|
||||
|
||||
# Returns all instance variables as a hash (including custom variables set at initialization).
|
||||
def config(arg=nil)
|
||||
if arg.nil?
|
||||
config_hash = {}
|
||||
self.instance_variables.each { |v| config_hash[v.delete("@").intern] = self.instance_variable_get(v) }
|
||||
config_hash
|
||||
else
|
||||
arg.each_pair {|k,v| self.instance_variable_set(("@" + k.to_s).intern, v)}
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,160 @@
|
|||
|
||||
module PacketFu
|
||||
|
||||
# EthOui is the Organizationally Unique Identifier portion of a MAC address, used in EthHeader.
|
||||
#
|
||||
# See the OUI list at http://standards.ieee.org/regauth/oui/oui.txt
|
||||
#
|
||||
# ==== Header Definition
|
||||
#
|
||||
# bit1 :b0
|
||||
# bit1 :b1
|
||||
# bit1 :b2
|
||||
# bit1 :b3
|
||||
# bit1 :b4
|
||||
# bit1 :b5
|
||||
# bit1 :local
|
||||
# bit1 :multicast
|
||||
# uint16be :oui, :initial_value => 0x1ac5 # :)
|
||||
class EthOui < BinData::MultiValue
|
||||
bit1 :b0
|
||||
bit1 :b1
|
||||
bit1 :b2
|
||||
bit1 :b3
|
||||
bit1 :b4
|
||||
bit1 :b5
|
||||
bit1 :local
|
||||
bit1 :multicast
|
||||
uint16be :oui, :initial_value => 0x1ac5 # :)
|
||||
end
|
||||
|
||||
# EthNic is the Network Interface Controler portion of a MAC address, used in EthHeader.
|
||||
#
|
||||
# ==== Header Definition
|
||||
#
|
||||
# unit8 :n1
|
||||
# unit8 :n2
|
||||
# unit8 :n3
|
||||
#
|
||||
class EthNic < BinData::MultiValue
|
||||
uint8 :n1
|
||||
uint8 :n2
|
||||
uint8 :n3
|
||||
end
|
||||
|
||||
# EthMac is the combination of an EthOui and EthNic, used in EthHeader.
|
||||
#
|
||||
# ==== Header Definition
|
||||
#
|
||||
# eth_oui :oui # See EthOui
|
||||
# eth_nic :nic # See EthOui
|
||||
class EthMac < BinData::MultiValue
|
||||
eth_oui :oui # See EthOui
|
||||
eth_nic :nic # See EthOui
|
||||
end
|
||||
|
||||
# EthHeader is a complete Ethernet struct, used in EthPacket.
|
||||
# It's the base header for all other protocols, such as IPHeader, TCPHeader, etc.
|
||||
#
|
||||
# For more on the construction on MAC addresses, see http://en.wikipedia.org/wiki/MAC_address
|
||||
#
|
||||
# ==== Header Definition
|
||||
#
|
||||
# eth_mac :eth_dst # See EthMac
|
||||
# eth_mac :eth_src # See EthMac
|
||||
# uint16be :eth_proto, :initial_value => 0x0800 # IP 0x0800, Arp 0x0806
|
||||
# rest :body
|
||||
class EthHeader < BinData::MultiValue
|
||||
eth_mac :eth_dst # See EthMac
|
||||
eth_mac :eth_src # See EthMac
|
||||
uint16be :eth_proto, :initial_value => 0x0800 # IP 0x0800, Arp 0x0806
|
||||
rest :body
|
||||
|
||||
# Set the source MAC address in a more readable way.
|
||||
def eth_saddr=(mac)
|
||||
mac = EthHeader.mac2str(mac)
|
||||
self.eth_src.read(mac)
|
||||
self.eth_src
|
||||
end
|
||||
|
||||
# Returns a more readable source MAC address.
|
||||
def eth_saddr
|
||||
EthHeader.str2mac(self.eth_src.to_s)
|
||||
end
|
||||
|
||||
# Set the destination MAC address in a more readable way.
|
||||
def eth_daddr=(mac)
|
||||
mac = EthHeader.mac2str(mac)
|
||||
self.eth_dst.read(mac)
|
||||
self.eth_dst
|
||||
end
|
||||
|
||||
# Returns a more readable source MAC address.
|
||||
def eth_daddr
|
||||
EthHeader.str2mac(self.eth_dst.to_s)
|
||||
end
|
||||
|
||||
# Converts a readable MAC (11:22:33:44:55:66) to a binary string. Readable MAC's may be split on colons, dots,
|
||||
# spaces, or underscores.
|
||||
#
|
||||
# irb> PacketFu::EthHeader.mac2str("11:22:33:44:55:66")
|
||||
#
|
||||
# #=> "\021\"3DUf"
|
||||
def self.mac2str(mac)
|
||||
if mac.split(/[:\x2d\x2e\x5f]/).size == 6
|
||||
ret = mac.split(/[:\x2d\x2e\x20\x5f]/).collect {|x| x.to_i(16)}.pack("C6")
|
||||
else
|
||||
raise ArgumentError, "Unkown format for mac address."
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
# Converts a binary string to a readable MAC (11:22:33:44:55:66).
|
||||
#
|
||||
# irb> PacketFu::EthHeader.str2mac("\x11\x22\x33\x44\x55\x66")
|
||||
#
|
||||
# #=> "11:22:33:44:55:66"
|
||||
def self.str2mac(mac)
|
||||
if mac.size == 6 && mac.class == String
|
||||
ret = mac.unpack("C6").collect {|x| sprintf("%02x",x)}.join(":")
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# EthPacket is used to construct Ethernet packets. They contain an EthHeader, and usually
|
||||
# other packet types.
|
||||
#
|
||||
# == Example
|
||||
#
|
||||
# require 'packetfu'
|
||||
# eth_pkt = PacketFu::EthPacket.new(:flavor => :apple)
|
||||
# eth_pkt.eth_saddr="01:02:03:04:05:06"
|
||||
# eth_pkt.eth_daddr="0a:0b:0c:0d:0e:0f"
|
||||
# eth_pkt.payload="I'm a lonely little eth packet with no real protocol information to speak of."
|
||||
# puts eth_pkt.to_f('/tmp/eth.pcap').inspect
|
||||
#
|
||||
# == Parameters
|
||||
#
|
||||
# :eth
|
||||
# A pre-generated EthHeader object. If not specified, a new one will be created.
|
||||
# :flavor
|
||||
# TODO: not implemented. Will generate EthPacket objects based on the OUI list.
|
||||
# :config
|
||||
# A hash of return address details, often the output of Utils.whoami?
|
||||
class EthPacket < Packet
|
||||
attr_accessor :eth_header
|
||||
|
||||
def ethernet?; true; end
|
||||
|
||||
def initialize(args={})
|
||||
@eth_header = (args[:eth] || EthHeader.new)
|
||||
|
||||
@headers = [@eth_header]
|
||||
super
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end # module PacketFu
|
|
@ -0,0 +1,112 @@
|
|||
|
||||
module PacketFu
|
||||
|
||||
# ICMPHeader is a complete ICMP struct, used in ICMPPacket. ICMP is typically used for network
|
||||
# administration and connectivity testing.
|
||||
#
|
||||
# For more on ICMP packets, see http://www.networksorcery.com/enp/protocol/icmp.htm
|
||||
#
|
||||
# ==== Header Definition
|
||||
#
|
||||
# uint8 :icmp_type
|
||||
# uint8 :icmp_code
|
||||
# uint16be :icmp_sum, :initial_value => lambda { icmp_calc_sum }
|
||||
# rest :body
|
||||
|
||||
class ICMPHeader < BinData::MultiValue
|
||||
uint8 :icmp_type
|
||||
uint8 :icmp_code
|
||||
uint16be :icmp_sum, :initial_value => lambda { icmp_calc_sum }
|
||||
rest :body
|
||||
|
||||
def icmp_calc_sum
|
||||
checksum = (icmp_type << 8) + icmp_code
|
||||
chk_body = (body.size % 2 == 0 ? body : body + "\x00")
|
||||
chk_body.scan(/[\x00-\xff]{2}/).collect { |x| (x[0] << 8) + x[1] }.each { |y| checksum += y }
|
||||
checksum = checksum % 0xffff
|
||||
checksum = 0xffff - checksum
|
||||
checksum == 0 ? 0xffff : checksum
|
||||
end
|
||||
|
||||
def icmp_recalc(arg=:all)
|
||||
case arg.intern
|
||||
when :icmp_sum
|
||||
self.icmp_sum=icmp_calc_sum
|
||||
when :all
|
||||
self.icmp_sum=icmp_calc_sum
|
||||
else
|
||||
raise ArgumentError, "No such field `#{arg}'"
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# ICMPPacket is used to construct ICMP Packets. They contain an EthHeader, an IPHeader, and a ICMPHeader.
|
||||
#
|
||||
# == Example
|
||||
#
|
||||
# icmp_pkt.new
|
||||
# icmp_pkt.icmp_type = 8
|
||||
# icmp_pkt.icmp_code = 0
|
||||
# icmp_pkt.payload = "ABC, easy as 123. As simple as do-re-mi. ABC, 123, baby, you and me!"
|
||||
#
|
||||
# icmp_pkt.ip_saddr="1.2.3.4"
|
||||
# icmp_pkt.ip_daddr="5.6.7.8"
|
||||
#
|
||||
# icmp_pkt.recalc
|
||||
# icmp_pkt.to_f('/tmp/icmp.pcap')
|
||||
#
|
||||
# == Parameters
|
||||
#
|
||||
# :eth
|
||||
# A pre-generated EthHeader object.
|
||||
# :ip
|
||||
# A pre-generated IPHeader object.
|
||||
# :flavor
|
||||
# TODO: Sets the "flavor" of the ICMP packet. Pings, in particular, often betray their true
|
||||
# OS.
|
||||
# :config
|
||||
# A hash of return address details, often the output of Utils.whoami?
|
||||
class ICMPPacket < Packet
|
||||
|
||||
attr_accessor :eth_header, :ip_header, :icmp_header
|
||||
|
||||
def ethernet?; true; end
|
||||
def ip?; true; end
|
||||
def icmp?; true; end
|
||||
|
||||
def initialize(args={})
|
||||
@eth_header = (args[:eth] || EthHeader.new)
|
||||
@ip_header = (args[:ip] || IPHeader.new)
|
||||
@icmp_header = (args[:icmp] || ICMPHeader.new)
|
||||
|
||||
@ip_header.body = @icmp_header
|
||||
@eth_header.body = @ip_header
|
||||
|
||||
@headers = [@eth_header, @ip_header, @icmp_header]
|
||||
super
|
||||
end
|
||||
|
||||
# Peek provides summary data on packet contents.
|
||||
def peek(args={})
|
||||
peek_data = ["C "] # I is taken by IP
|
||||
peek_data << "%-5d" % self.to_s.size
|
||||
type = case self.icmp_type
|
||||
when 8
|
||||
"ping"
|
||||
when 0
|
||||
"pong"
|
||||
else
|
||||
"%02x-%02x" % [self.icmp_type, self.icmp_code]
|
||||
end
|
||||
peek_data << "%-21s" % "#{self.ip_saddr}:#{type}"
|
||||
peek_data << "->"
|
||||
peek_data << "%21s" % "#{self.ip_daddr}"
|
||||
peek_data << "%23s" % "I:"
|
||||
peek_data << "%04x" % self.ip_id
|
||||
peek_data.join
|
||||
end
|
||||
end
|
||||
|
||||
end # module PacketFu
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
|
||||
module PacketFu
|
||||
|
||||
# The Inject class handles injecting arrays of binary data on the wire.
|
||||
#
|
||||
# To inject single packets, use PacketFu::Packet.to_w() instead.
|
||||
class Inject
|
||||
attr_accessor :array, :stream, :show_live # Leave these public and open.
|
||||
attr_reader :iface, :snaplen, :promisc, :timeout # Cant change after the init.
|
||||
|
||||
def initialize(args={})
|
||||
@array = [] # Where the packet array goes.
|
||||
@stream = [] # Where the stream goes.
|
||||
@iface = args[:iface] || Pcap.lookupdev || 'lo'
|
||||
@snaplen = args[:snaplen] || 0xffff
|
||||
@promisc = args[:promisc] || false # Sensible for some Intel wifi cards
|
||||
@timeout = args[:timeout] || 1
|
||||
@show_live = nil
|
||||
end
|
||||
|
||||
# Takes an array, and injects them onto an interface. Note that
|
||||
# complete packets (Ethernet headers on down) are expected.
|
||||
#
|
||||
# === Parameters
|
||||
#
|
||||
# :array || arr
|
||||
# An array of binary data (usually packet.to_s style).
|
||||
# :int || sleep
|
||||
# Number of seconds to sleep between injections (in float format)
|
||||
# :show_live || :live
|
||||
# If true, puts data about what was injected to stdout.
|
||||
#
|
||||
# === Example
|
||||
#
|
||||
# inj = PacketFu::Inject.new
|
||||
# inj.array_to_wire(:array => [pkt1, pkt2, pkt3], :sleep => 0.1)
|
||||
#
|
||||
def array_to_wire(args={})
|
||||
pkt_array = args[:array] || args[:arr] || @array
|
||||
interval = args[:int] || args[:sleep]
|
||||
show_live = args[:show_live] || args[:live] || @show_live
|
||||
|
||||
@stream = Pcap.open_live(@iface,@snaplen,@promisc,@timeout)
|
||||
pkt_count = 0
|
||||
pkt_array.each do |pkt|
|
||||
@stream.inject(pkt)
|
||||
sleep interval if interval
|
||||
pkt_count +=1
|
||||
puts "Sent Packet \##{pkt_count} (#{pkt.size})" if show_live
|
||||
end
|
||||
# Return # of packets sent, array size, and array total size
|
||||
[pkt_count, pkt_array.size, pkt_array.join.size]
|
||||
end
|
||||
|
||||
# Equivalent to array_to_wire
|
||||
def a2w(args={})
|
||||
array_to_wire(args)
|
||||
end
|
||||
|
||||
# Equivalent to array_to_wire
|
||||
def inject(args={})
|
||||
array_to_wire(args)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
|
||||
module PacketFu
|
||||
|
||||
# InvalidHeader catches all packets that we don't already have a struct for, or
|
||||
# for whatever reason, violates some basic packet rules for other packet types.
|
||||
class InvalidHeader < BinData::MultiValue
|
||||
rest :body # No idea how big this will be or what it will look like; it's invalid!
|
||||
end
|
||||
|
||||
# You probably don't want to write invalid packets on purpose.
|
||||
class InvalidPacket < Packet
|
||||
|
||||
attr_accessor :invalid_header
|
||||
|
||||
def invalid?; true; end
|
||||
|
||||
def initialize(args={})
|
||||
@invalid_header = (args[:invalid] || InvalidHeader.new)
|
||||
@headers = [@invalid_header]
|
||||
end
|
||||
end
|
||||
|
||||
end # module PacketFu
|
|
@ -0,0 +1,229 @@
|
|||
|
||||
module PacketFu
|
||||
|
||||
# Octets implements the addressing scheme for IP.
|
||||
#
|
||||
# ==== Header Definition
|
||||
#
|
||||
# uint8 :o1
|
||||
# uint8 :o2
|
||||
# uint8 :o3
|
||||
# uint8 :o4
|
||||
class Octets < BinData::MultiValue
|
||||
uint8 :o1
|
||||
uint8 :o2
|
||||
uint8 :o3
|
||||
uint8 :o4
|
||||
|
||||
# Returns an address in dotted-quad format.
|
||||
def to_x
|
||||
ip_str = [o1, o2, o3, o4].join('.')
|
||||
IPAddr.new(ip_str).to_s
|
||||
end
|
||||
|
||||
# Returns an address in numerical format.
|
||||
def to_i
|
||||
ip_str = [o1, o2, o3, o4].join('.')
|
||||
IPAddr.new(ip_str).to_i
|
||||
end
|
||||
|
||||
# Returns an address as an array of numbers.
|
||||
def to_ary
|
||||
[o1,o2,o3,o4]
|
||||
end
|
||||
|
||||
alias to_a to_ary
|
||||
end
|
||||
|
||||
# IPHeader is a complete IP struct, used in IPPacket. Most traffic on most networks today is IP-based.
|
||||
#
|
||||
# For more on IP packets, see http://www.networksorcery.com/enp/protocol/ip.htm
|
||||
#
|
||||
# ==== Header Definition
|
||||
#
|
||||
# bit4 :ip_v, :initial_value => 4
|
||||
# bit4 :ip_hl, :initial_value => 5
|
||||
# uint8 :ip_tos, :initial_value => 0 # TODO: Break out the bits
|
||||
# uint16be :ip_len, :initial_value => lambda { ip_calc_len }
|
||||
# uint16be :ip_id, :initial_value => lambda { ip_calc_id } # IRL, hardly random.
|
||||
# uint16be :ip_frag, :initial_value => 0 # TODO: Break out the bits
|
||||
# uint8 :ip_ttl, :initial_value => 0xff # Changes per flavor
|
||||
# uint8 :ip_proto, :initial_value => 0x01 # TCP: 0x06, UDP 0x11, ICMP 0x01
|
||||
# uint16be :ip_sum, :initial_value => lambda { ip_calc_sum }
|
||||
# octets :ip_src # No value as this is a MultiValue
|
||||
# octets :ip_dst # Ditto.
|
||||
# rest :body
|
||||
class IPHeader < BinData::MultiValue
|
||||
|
||||
bit4 :ip_v, :initial_value => 4
|
||||
bit4 :ip_hl, :initial_value => 5
|
||||
uint8 :ip_tos, :initial_value => 0 # TODO: Break out the bits
|
||||
uint16be :ip_len, :initial_value => lambda { ip_calc_len }
|
||||
uint16be :ip_id, :initial_value => lambda { ip_calc_id } # IRL, hardly random.
|
||||
uint16be :ip_frag, :initial_value => 0 # TODO: Break out the bits
|
||||
uint8 :ip_ttl, :initial_value => 0xff # Changes per flavor
|
||||
uint8 :ip_proto,:initial_value => 0x01 # TCP: 0x06, UDP 0x11, ICMP 0x01
|
||||
uint16be :ip_sum, :initial_value => lambda { ip_calc_sum }
|
||||
octets :ip_src # No value as this is a MultiValue
|
||||
octets :ip_dst # Ditto.
|
||||
rest :body
|
||||
|
||||
# Creates a new IPHeader object, and intialize with a random IPID.
|
||||
def initialize(*args)
|
||||
@random_id = rand(0xffff)
|
||||
super
|
||||
end
|
||||
|
||||
# Calulcate the true length of the packet.
|
||||
def ip_calc_len
|
||||
(ip_hl * 4) + body.to_s.length
|
||||
end
|
||||
|
||||
# Calculate the true checksum of the packet.
|
||||
# (Yes, this is the long way to do it, but it's e-z-2-read for mathtards like me.)
|
||||
def ip_calc_sum
|
||||
checksum = (((ip_v << 4) + ip_hl) << 8) +ip_tos
|
||||
checksum += ip_len
|
||||
checksum += ip_id
|
||||
checksum += ip_frag
|
||||
checksum += (ip_ttl << 8) + ip_proto
|
||||
checksum += (ip_src.to_i >> 16)
|
||||
checksum += (ip_src.to_i & 0xffff)
|
||||
checksum += (ip_dst.to_i >> 16)
|
||||
checksum += (ip_dst.to_i & 0xffff)
|
||||
checksum = checksum % 0xffff
|
||||
checksum = 0xffff - checksum
|
||||
checksum == 0 ? 0xffff : checksum
|
||||
end
|
||||
|
||||
# Retrieve the IP ID
|
||||
def ip_calc_id
|
||||
@random_id
|
||||
end
|
||||
|
||||
# Sets a more readable IP address. If you wants to manipulate individual octets,
|
||||
# (eg, for host scanning in one network), it would be better use ip_src.o1 through
|
||||
# ip_src.o4 instead.
|
||||
def ip_saddr=(addr)
|
||||
addr = IPHeader.octet_array(addr)
|
||||
ip_src.o1 = addr[0]
|
||||
ip_src.o2 = addr[1]
|
||||
ip_src.o3 = addr[2]
|
||||
ip_src.o4 = addr[3]
|
||||
end
|
||||
|
||||
# Returns a more readable IP source address.
|
||||
def ip_saddr
|
||||
[ip_src.o1,ip_src.o2,ip_src.o3,ip_src.o4].join('.')
|
||||
end
|
||||
|
||||
# Sets a more readable IP address.
|
||||
def ip_daddr=(addr)
|
||||
addr = IPHeader.octet_array(addr)
|
||||
ip_dst.o1 = addr[0]
|
||||
ip_dst.o2 = addr[1]
|
||||
ip_dst.o3 = addr[2]
|
||||
ip_dst.o4 = addr[3]
|
||||
end
|
||||
|
||||
# Returns a more readable IP destination address.
|
||||
def ip_daddr
|
||||
[ip_dst.o1,ip_dst.o2,ip_dst.o3,ip_dst.o4].join('.')
|
||||
end
|
||||
|
||||
|
||||
# Translate various formats of IPv4 Addresses to an array of digits.
|
||||
def self.octet_array(addr)
|
||||
if addr.class == String
|
||||
oa = addr.split('.').collect {|x| x.to_i}
|
||||
elsif addr.class == Fixnum
|
||||
oa = IPAddr.new(addr, Socket::AF_INET).to_s.split('.')
|
||||
elsif addr.class == Bignum
|
||||
oa = IPAddr.new(addr, Socket::AF_INET).to_s.split('.')
|
||||
elsif addr.class == Array
|
||||
oa = addr
|
||||
else
|
||||
raise ArgumentError, "IP Address should be a dotted quad string, an array of ints, or a bignum"
|
||||
end
|
||||
end
|
||||
|
||||
# Recalculate the calculated IP fields. Valid arguments are:
|
||||
# :all :ip_len :ip_sum :ip_id
|
||||
def ip_recalc(arg=:all)
|
||||
case arg
|
||||
when :ip_len
|
||||
self.ip_len=ip_calc_len
|
||||
when :ip_sum
|
||||
self.ip_sum=ip_calc_sum
|
||||
when :ip_id
|
||||
@random_id = rand(0xffff)
|
||||
when :all
|
||||
self.ip_id= ip_calc_id
|
||||
self.ip_len= ip_calc_len
|
||||
self.ip_sum= ip_calc_sum
|
||||
else
|
||||
raise ArgumentError, "No such field `#{arg}'"
|
||||
end
|
||||
end
|
||||
end # class IPHeader
|
||||
|
||||
# IPPacket is used to construct IP packets. They contain an EthHeader, an IPHeader, and usually
|
||||
# a transport-layer protocol such as UDPHeader, TCPHeader, or ICMPHeader.
|
||||
#
|
||||
# == Example
|
||||
#
|
||||
# require 'packetfu'
|
||||
# ip_pkt = PacketFu::IPPacket.new
|
||||
# ip_pkt.ip_saddr="10.20.30.40"
|
||||
# ip_pkt.ip_daddr="192.168.1.1"
|
||||
# ip_pkt.ip_proto=1
|
||||
# ip_pkt.ip_ttl=64
|
||||
# ip_pkt.ip_payload="\x00\x00\x12\x34\x00\x01\x00\x01"+
|
||||
# "Lovingly hand-crafted echo responses delivered directly to your door."
|
||||
# ip_pkt.recalc
|
||||
# ip_pkt.to_f('/tmp/ip.pcap')
|
||||
#
|
||||
# == Parameters
|
||||
#
|
||||
# :eth
|
||||
# A pre-generated EthHeader object.
|
||||
# :ip
|
||||
# A pre-generated IPHeader object.
|
||||
# :flavor
|
||||
# TODO: Sets the "flavor" of the IP packet. This might include known sets of IP options, and
|
||||
# certainly known starting TTLs.
|
||||
# :config
|
||||
# A hash of return address details, often the output of Utils.whoami?
|
||||
class IPPacket < Packet
|
||||
|
||||
attr_accessor :eth_header, :ip_header
|
||||
|
||||
def ethernet?; true; end
|
||||
def ip?; true; end
|
||||
|
||||
# Creates a new IPPacket object.
|
||||
def initialize(args={})
|
||||
@eth_header = (args[:eth] || EthHeader.new)
|
||||
@ip_header = (args[:ip] || IPHeader.new)
|
||||
@eth_header.body=@ip_header
|
||||
|
||||
@headers = [@eth_header, @ip_header]
|
||||
super
|
||||
|
||||
end
|
||||
|
||||
# Peek provides summary data on packet contents.
|
||||
def peek(args={})
|
||||
peek_data = ["I "]
|
||||
peek_data << "%-5d" % self.to_s.size
|
||||
peek_data << "%-21s" % "#{self.ip_saddr}"
|
||||
peek_data << "->"
|
||||
peek_data << "%21s" % "#{self.ip_daddr}"
|
||||
peek_data << "%23s" % "I:"
|
||||
peek_data << "%04x" % self.ip_id
|
||||
peek_data.join
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end # module PacketFu
|
|
@ -0,0 +1,138 @@
|
|||
|
||||
module PacketFu
|
||||
|
||||
# AddrIpv6 handles addressing for IPv6Header
|
||||
#
|
||||
# ==== Header Definition
|
||||
#
|
||||
#
|
||||
# uint32be :a1
|
||||
# uint32be :a2
|
||||
# uint32be :a3
|
||||
# uint32be :a4
|
||||
class AddrIpv6 < BinData::MultiValue
|
||||
|
||||
uint32be :a1
|
||||
uint32be :a2
|
||||
uint32be :a3
|
||||
uint32be :a4
|
||||
|
||||
end
|
||||
|
||||
# IPv6Header is complete IPv6 struct, used in IPv6Packet.
|
||||
#
|
||||
# ==== Header Definition
|
||||
#
|
||||
# bit4 :ipv6_v, :initial_value => 6 # Versiom
|
||||
# bit8 :ipv6_class # Class
|
||||
# bit20 :ipv6_label # Label
|
||||
# uint16be :ipv6_len, :initial_value => lambda { ipv6_calc_len } # Payload length
|
||||
# uint8 :ipv6_next # Next Header
|
||||
# uint8 :ipv6_hop, :initial_value => 0xff # Hop limit
|
||||
# addr_ipv6 :ipv6_src
|
||||
# addr_ipv6 :ipv6_dst
|
||||
# rest :body
|
||||
class IPv6Header < BinData::MultiValue
|
||||
|
||||
bit4 :ipv6_v, :initial_value => 6 # Versiom
|
||||
bit8 :ipv6_class # Class
|
||||
bit20 :ipv6_label # Label
|
||||
uint16be :ipv6_len, :initial_value => lambda { ipv6_calc_len } # Payload length
|
||||
uint8 :ipv6_next # Next Header
|
||||
uint8 :ipv6_hop, :initial_value => 0xff # Hop limit
|
||||
addr_ipv6 :ipv6_src
|
||||
addr_ipv6 :ipv6_dst
|
||||
rest :body
|
||||
|
||||
def ipv6_calc_len
|
||||
ipv6_len = self.body.size
|
||||
end
|
||||
|
||||
def ipv6_recalc(arg=:all)
|
||||
case arg
|
||||
when :ipv6_len
|
||||
ipv6_calc_len
|
||||
when :all
|
||||
ipv6_recalc(:len)
|
||||
end
|
||||
end
|
||||
|
||||
# Presents in a more readable form.
|
||||
def ipv6_saddr
|
||||
addr = [self.ipv6_src.a1,self.ipv6_src.a2,self.ipv6_src.a3,self.ipv6_src.a4].pack("NNNN")
|
||||
addr.unpack("H*")[0].scan(/.{8}/).collect {|x| x.sub(/^0*([0-9a-f])/,"\\1")}.join(":")
|
||||
end
|
||||
|
||||
# Takes in a more readable form. Leading zero compression is fine, but that's it. :(
|
||||
def ipv6_saddr=(str)
|
||||
arr = str.split(':').collect {|x| x.to_i(16)}
|
||||
self.ipv6_src.a1 = arr[0]
|
||||
self.ipv6_src.a2 = arr[1]
|
||||
self.ipv6_src.a3 = arr[2]
|
||||
self.ipv6_src.a4 = arr[3]
|
||||
end
|
||||
|
||||
# Presents in a more readable form.
|
||||
def ipv6_daddr
|
||||
addr = [self.ipv6_dst.a1,self.ipv6_dst.a2,self.ipv6_dst.a3,self.ipv6_dst.a4].pack("NNNN")
|
||||
addr.unpack("H*")[0].scan(/.{8}/).collect {|x| x.sub(/^0*([0-9a-f])/,"\\1")}.join(":")
|
||||
end
|
||||
|
||||
# Takes in a more readable form. Leading zero compression is fine, but that's it. :(
|
||||
def ipv6_daddr=(str)
|
||||
arr = str.split(':').collect {|x| x.to_i(16)}
|
||||
self.ipv6_dst.a1 = arr[0]
|
||||
self.ipv6_dst.a2 = arr[1]
|
||||
self.ipv6_dst.a3 = arr[2]
|
||||
self.ipv6_dst.a4 = arr[3]
|
||||
end
|
||||
|
||||
end # class IPv6Header
|
||||
|
||||
# IPv6Packet is used to construct IPv6 Packets. They contain an EthHeader and an IPv6Header, and in
|
||||
# the distant, unknowable future, will take interesting IPv6ish payloads.
|
||||
#
|
||||
# This mostly complete, but not very useful. It's intended primarily as an example protocol.
|
||||
#
|
||||
# == Parameters
|
||||
#
|
||||
# :eth
|
||||
# A pre-generated EthHeader object.
|
||||
# :ip
|
||||
# A pre-generated IPHeader object.
|
||||
# :flavor
|
||||
# TODO: Sets the "flavor" of the IPv6 packet. No idea what this will look like, haven't done much IPv6 fingerprinting.
|
||||
# :config
|
||||
# A hash of return address details, often the output of Utils.whoami?
|
||||
class IPv6Packet < Packet
|
||||
|
||||
attr_accessor :eth_header, :ipv6_header
|
||||
|
||||
def ethernet?; true; end
|
||||
def ipv6?; true; end
|
||||
|
||||
def initialize(args={})
|
||||
@eth_header = (args[:eth] || EthHeader.new)
|
||||
@ipv6_header = (args[:ipv6] || IPv6Header.new)
|
||||
@eth_header.eth_proto = 0x86dd
|
||||
@eth_header.body=@ipv6_header
|
||||
|
||||
@headers = [@eth_header, @arp_header]
|
||||
super
|
||||
end
|
||||
|
||||
# Peek provides summary data on packet contents.
|
||||
def peek(args={})
|
||||
peek_data = ["6 "]
|
||||
peek_data << "%-5d" % self.to_s.size
|
||||
peek_data << self.ipv6_saddr
|
||||
peek_data << "->"
|
||||
peek_data << self.ipv6_daddr
|
||||
peek_data << " N:"
|
||||
peek_data << self.ipv6_next.to_s(16)
|
||||
peek_data.join
|
||||
end
|
||||
|
||||
end # class IPv6Packet
|
||||
|
||||
end # module PacketFu
|
|
@ -0,0 +1,336 @@
|
|||
|
||||
module PacketFu
|
||||
|
||||
# Packet is the parent class of EthPacket, IPPacket, UDPPacket, TCPPacket, and all
|
||||
# other packets.
|
||||
class Packet
|
||||
attr_reader :flavor # Packet Headers are responsible for their own specific flavor methods.
|
||||
|
||||
# Parse() creates the correct packet type based on the data, and returns the apporpiate
|
||||
# Packet subclass.
|
||||
#
|
||||
# There is an assumption here that all incoming packets are either EthPacket
|
||||
# or InvalidPacket types.
|
||||
#
|
||||
# New packet types should get an entry here.
|
||||
def self.parse(packet,args={})
|
||||
if packet.size >= 14 # Min size for Ethernet. No check for max size, yet.
|
||||
case packet[12,2] # Check the Eth protocol field.
|
||||
when "\x08\x00" # It's IP.
|
||||
case (packet[14,1][0] >> 4) # Check the IP version field.
|
||||
when 4; # It's IPv4.
|
||||
case packet[23,1] # Check the IP protocol field.
|
||||
when "\x06"; p = TCPPacket.new # Returns a TCPPacket.
|
||||
when "\x11"; p = UDPPacket.new # Returns a UDPPacket.
|
||||
when "\x01"; p = ICMPPacket.new # Returns an ICMPPacket.
|
||||
else; p = IPPacket.new # Returns an IPPacket since we can't tell the transport layer.
|
||||
end
|
||||
else; p = EthPacket.new # Returns an EthPacket since we don't know any other IP version.
|
||||
end
|
||||
when "\x08\x06" # It's arp
|
||||
if packet.size >= 28 # Min size for complete arp
|
||||
p = ARPPacket.new
|
||||
else; p = EthPacket.new # Returns an EthPacket since we can't deal with tiny arps.
|
||||
end
|
||||
when "\x86\xdd" # It's IPv6
|
||||
if packet.size >= 54 # Min size for a complete IPv6 packet.
|
||||
p = IPv6Packet.new
|
||||
else; p = EthPacket.new # Returns an EthPacket since we can't deal with tiny Ipv6.
|
||||
end
|
||||
else; p = EthPacket.new # Returns an EthPacket since we can't tell the network layer.
|
||||
end
|
||||
else
|
||||
p = InvalidPacket.new # Not the right size for Ethernet (jumbo frames are okay)
|
||||
end
|
||||
p.read(packet,args)
|
||||
return p
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# These methods are overridden for specific types of packets
|
||||
# This allows easy identification of the packet type
|
||||
#
|
||||
def ip?; false; end
|
||||
def tcp?; false; end
|
||||
def udp?; false; end
|
||||
def icmp?; false; end
|
||||
def arp?; false; end
|
||||
def ipv6?; false; end
|
||||
def ethernet?; false; end
|
||||
def invalid?; false; end
|
||||
|
||||
#method_missing() delegates protocol-specific field actions to the apporpraite
|
||||
#class variable (which contains the associated packet type)
|
||||
#This register-of-protocols style switch will work for the
|
||||
#forseeable future (there aren't /that/ many packet types), and it's a handy
|
||||
#way to know at a glance what packet types are supported.
|
||||
def method_missing(sym, *args)
|
||||
case sym.to_s
|
||||
when /^invalid_/
|
||||
@invalid_header.send(sym,*args)
|
||||
when /^eth_/
|
||||
@eth_header.send(sym,*args)
|
||||
when /^arp_/
|
||||
@arp_header.send(sym,*args)
|
||||
when /^ip_/
|
||||
@ip_header.send(sym,*args)
|
||||
when /^icmp_/
|
||||
@icmp_header.send(sym,*args)
|
||||
when /^udp_/
|
||||
@udp_header.send(sym,*args)
|
||||
when /^tcp_/
|
||||
@tcp_header.send(sym,*args)
|
||||
when /^ipv6_/
|
||||
@ipv6_header.send(sym,*args)
|
||||
else
|
||||
raise NoMethodError, "Unknown method `#{sym}' for this packet object."
|
||||
end
|
||||
end
|
||||
|
||||
# Get the binary string of the entire packet.
|
||||
def to_s
|
||||
@headers[0].to_s
|
||||
end
|
||||
|
||||
# In the event of no proper decoding, at least send it to the inner-most header.
|
||||
def read(io)
|
||||
@headers[0].read(io)
|
||||
end
|
||||
|
||||
# In the event of no proper decoding, at least send it to the inner-most header.
|
||||
def write(io)
|
||||
@headers[0].write(io)
|
||||
end
|
||||
|
||||
# Get the outermost payload (body) of the packet; this is why all packet headers
|
||||
# should have a body type.
|
||||
def payload
|
||||
@headers.last.body
|
||||
end
|
||||
|
||||
# Set the outermost payload (body) of the packet.
|
||||
def payload=(args)
|
||||
@headers.last.body=(args)
|
||||
end
|
||||
|
||||
# Put the entire packet into a libpcap file.
|
||||
def to_f(filename=nil)
|
||||
PacketFu::Write.a2f(:file=> filename || PacketFu::Config.new.config[:pcapfile],
|
||||
:arr=>[@headers[0].to_s])
|
||||
end
|
||||
|
||||
# Put the entire packet on the wire by creating a temporary PacketFu::Inject object.
|
||||
# TODO: Do something with auto-checksumming?
|
||||
def to_w(iface=nil)
|
||||
inj = PacketFu::Inject.new(:iface => (iface || PacketFu::Config.new.config[:iface]))
|
||||
inj.array = [@headers[0].to_s]
|
||||
inj.inject
|
||||
end
|
||||
|
||||
# Recalculates all the calcuated fields for all headers in the packet.
|
||||
# This is important since read() wipes out all the calculated fields
|
||||
# such as length and checksum and what all.
|
||||
# TODO: Is there a better way to ensure I get the correct checksum?
|
||||
# This way is pretty easy; third time is, indeed, the charm.
|
||||
def recalc(arg=:all)
|
||||
3.times do # XXX: This is a silly fix, surely there's a better way.
|
||||
case arg
|
||||
when :ip
|
||||
ip_recalc(:all)
|
||||
when :udp
|
||||
udp_recalc(:all)
|
||||
when :tcp
|
||||
tcp_recalc(:all)
|
||||
when :all
|
||||
ip_recalc(:all) if @ip_header
|
||||
udp_recalc(:all) if @udp_header
|
||||
tcp_recalc(:all) if @tcp_header
|
||||
else
|
||||
raise ArgumentError, "Recalculating `#{arg}' unsupported. Try :all"
|
||||
end
|
||||
end
|
||||
@headers[0]
|
||||
end
|
||||
|
||||
# Read() takes (and trusts) the io input and shoves it all into a well-formed Packet.
|
||||
# Note that read is a destructive process, so any existing data will be lost.
|
||||
#
|
||||
# TODO: This giant if tree is a mess, and worse, is decieving. You need to define
|
||||
# actions both here and in parse(). All read() does is make a (good) guess as to
|
||||
# what @headers to expect, and reads data to them.
|
||||
#
|
||||
# To take strings and turn them into packets without knowing ahead of time what kind of
|
||||
# packet it is, use Packet.parse instead; parse() handles the figuring-out part.
|
||||
#
|
||||
# A note on the :strip => true argument: If :strip is set, defined lengths of data will
|
||||
# be believed, and any trailers (such as frame check sequences) will be chopped off. This
|
||||
# helps to ensure well-formed packets, at the cost of losing perhaps important FCS data.
|
||||
#
|
||||
# If :strip is false, header lengths are /not/ believed, and all data will be piped in.
|
||||
# When capturing from the wire, this is usually fine, but recalculating the length before
|
||||
# saving or re-transmitting will absolutely change the data payload; FCS data will become
|
||||
# part of the TCP data as far as tcp_len is concerned. Some effort has been made to preserve
|
||||
# the "real" payload for the purposes of checksums, but currently, it's impossible to seperate
|
||||
# new payload data from old trailers, so things like pkt.payload += "some data" will not work
|
||||
# correctly.
|
||||
#
|
||||
# So, to summarize; if you intend to alter the data, use :strip. If you don't, don't.
|
||||
def read(io,args={})
|
||||
if io.size >= 14
|
||||
@eth_header.read(io[0,14])
|
||||
eth_proto_num = io[12,2].unpack("n")[0]
|
||||
if eth_proto_num == 0x0800 # It's IP.
|
||||
ip_hlen=(io[14] & 0x0f) * 4
|
||||
ip_proto_num = io[23,1].unpack("C")[0]
|
||||
@ip_header.read(io[14,ip_hlen])
|
||||
@eth_header.body = @ip_header
|
||||
if ip_proto_num == 0x06 # It's TCP.
|
||||
tcp_len = io[16,2].unpack("n")[0] - 20
|
||||
if args[:strip] # Drops trailers like frame check sequence (FCS). Often desired for cleaner packets.
|
||||
tcp_all = io[ip_hlen+14,tcp_len] # Believe the tcp_len value; chop off anything that's not in range.
|
||||
else
|
||||
tcp_all = io[ip_hlen+14,0xffff] # Don't believe the tcp_len value; suck everything up.
|
||||
end
|
||||
tcp_hlen = ((tcp_all[12,1].unpack("C")[0]) >> 4) * 4
|
||||
tcp_opts = tcp_all[20,tcp_hlen-20]
|
||||
tcp_body = tcp_all[tcp_hlen,0xffff]
|
||||
@tcp_header.read(tcp_all[0,20])
|
||||
@tcp_header.tcp_opts=tcp_opts
|
||||
@tcp_header.body=tcp_body
|
||||
@ip_header.body = @tcp_header
|
||||
elsif ip_proto_num == 0x11 # It's UDP.
|
||||
udp_len = io[16,2].unpack("n")[0] - 20
|
||||
if args[:strip] # Same deal as with TCP. We might have stuff at the end of the packet that's not part of the payload.
|
||||
@udp_header.read(io[ip_hlen+14,udp_len])
|
||||
else # ... Suck it all up. BTW, this will change the lengths if they are ever recalc'ed. Bummer.
|
||||
@udp_header.read(io[ip_hlen+14,0xffff])
|
||||
end
|
||||
@ip_header.body = @udp_header
|
||||
elsif ip_proto_num == 1 # It's ICMP
|
||||
@icmp_header.read(io[ip_hlen+14,0xffff])
|
||||
@ip_header.body = @icmp_header
|
||||
else # It's an IP packet for a protocol we don't have a decoder for.
|
||||
@ip_header.body = io[16,io.size-16]
|
||||
end
|
||||
@eth_header.body = @ip_header
|
||||
elsif eth_proto_num == 0x0806 # It's ARP
|
||||
@arp_header.read(io[14,0xffff]) # You'll nearly have a trailer and you'll never know what size.
|
||||
@eth_header.body=@arp_header
|
||||
elsif eth_proto_num == 0x86dd # It's IPv6
|
||||
@ipv6_header.read(io[14,0xffff])
|
||||
@eth_header.body=@ipv6_header
|
||||
else # It's an Ethernet packet for a protocol we don't have a decoder for
|
||||
@eth_header.body = io[14,io.size-14]
|
||||
end
|
||||
if (args[:fix] || args[:recalc])
|
||||
# Unfortunately, we cannot simply recalc with abandon, since
|
||||
# we may have unaccounted trailers that will sneak into the checksum.
|
||||
# The better way to handle this is to put trailers in their own
|
||||
# BinData field, but I'm not a-gonna right now. :/
|
||||
ip_recalc(:ip_sum) if respond_to? :ip_header
|
||||
recalc(:tcp) if respond_to? :tcp_header
|
||||
recalc(:udp) if respond_to? :udp_header
|
||||
end
|
||||
else # You're not big enough for Ethernet.
|
||||
@invalid_header.read(io)
|
||||
end
|
||||
@headers[0]
|
||||
end
|
||||
|
||||
# Peek provides summary data on packet contents.
|
||||
# Each packet type should provide its own peek method, and shouldn't exceed 80 characters wide (for
|
||||
# easy reading in normal irb shells). If they don't, this default summary will step in.
|
||||
def peek(args={})
|
||||
peek_data = ["? "]
|
||||
peek_data << "%-5d" % self.to_s.size
|
||||
peek_data << "%68s" % self.to_s[0,34].unpack("H*")[0]
|
||||
peek_data.join
|
||||
end
|
||||
|
||||
# Hexify provides a neatly-formatted dump of binary data, familar to hex readers.
|
||||
def hexify(str)
|
||||
hexascii_lines = str.to_s.unpack("H*")[0].scan(/.{1,32}/)
|
||||
chars = str.to_s.gsub(/[\x00-\x1f\x7f-\xff]/,'.')
|
||||
chars_lines = chars.scan(/.{1,16}/)
|
||||
ret = []
|
||||
hexascii_lines.size.times {|i| ret << "%-48s %s" % [hexascii_lines[i].gsub(/(.{2})/,"\\1 "),chars_lines[i]]}
|
||||
ret.join("\n")
|
||||
end
|
||||
|
||||
# Returns a hex-formatted representation of the packet.
|
||||
#
|
||||
# ==== Arguments
|
||||
#
|
||||
# 0..9 : If a number is given only the layer in @header[arg] will be displayed. Note that this will include all @headers included in that header.
|
||||
# :layers : If :layers is specified, the dump will return an array of headers by layer level.
|
||||
# :all : An alias for arg=0.
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# irb(main):003:0> pkt = TCPPacket.new
|
||||
# irb(main):003:0> puts pkt.inspect_hex(:layers)
|
||||
# 00 1a c5 00 00 00 00 1a c5 00 00 00 08 00 45 00 ..............E.
|
||||
# 00 28 83 ce 00 00 ff 06 38 02 00 00 00 00 00 00 .(......8.......
|
||||
# 00 00 a6 0f 00 00 ac 89 7b 26 00 00 00 00 50 00 ........{&....P.
|
||||
# 40 00 a2 25 00 00 @..%..
|
||||
# 45 00 00 28 83 ce 00 00 ff 06 38 02 00 00 00 00 E..(......8.....
|
||||
# 00 00 00 00 a6 0f 00 00 ac 89 7b 26 00 00 00 00 ..........{&....
|
||||
# 50 00 40 00 a2 25 00 00 P.@..%..
|
||||
# a6 0f 00 00 ac 89 7b 26 00 00 00 00 50 00 40 00 ......{&....P.@.
|
||||
# a2 25 00 00 .%..
|
||||
# => nil
|
||||
# irb(main):004:0> puts pkt.inspect_hex(:layers)[2]
|
||||
# a6 0f 00 00 ac 89 7b 26 00 00 00 00 50 00 40 00 ......{&....P.@.
|
||||
# a2 25 00 00 .%..
|
||||
# => nil
|
||||
#
|
||||
def inspect_hex(arg=0)
|
||||
case arg
|
||||
when :layers
|
||||
ret = []
|
||||
@headers.size.times do |i|
|
||||
ret << hexify(@headers[i])
|
||||
end
|
||||
ret
|
||||
when (0..9)
|
||||
if @headers[arg]
|
||||
hexify(@headers[arg])
|
||||
else
|
||||
nil
|
||||
end
|
||||
when :all
|
||||
inspect_hex(0)
|
||||
end
|
||||
end
|
||||
|
||||
# For packets, inspect is overloaded as inspect_hex(0).
|
||||
# Not sure if this is a great idea yet, but it sure makes
|
||||
# the irb output more sane.
|
||||
def inspect
|
||||
self.inspect_hex
|
||||
end
|
||||
|
||||
# Returns the size of the packet (as a binary string)
|
||||
def size
|
||||
self.to_s.size
|
||||
end
|
||||
|
||||
alias_method :length, :size
|
||||
|
||||
def initialize(args={})
|
||||
if args[:config]
|
||||
args[:config].each_pair do |k,v|
|
||||
case k
|
||||
when :eth_daddr; @eth_header.eth_daddr=v if @eth_header
|
||||
when :eth_saddr; @eth_header.eth_saddr=v if @eth_header
|
||||
when :ip_saddr; @ip_header.ip_saddr=v if @ip_header
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end # class Packet
|
||||
|
||||
end # module PacketFu
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
module PacketFu
|
||||
|
||||
|
||||
# PcapHeader describes the libpcap file header format, and is used in PcapFile.
|
||||
class PcapHeader < BinData::MultiValue
|
||||
string :magic, :length => 4, :initial_value => "\xd4\xc3\xb2\xa1"
|
||||
uint16le :ver_major, :initial_value => 2
|
||||
uint16le :ver_minor, :initial_value => 4
|
||||
int32le :thiszone, :initial_value => 0
|
||||
uint32le :sigfigs, :initial_value => 0
|
||||
uint32le :snaplen, :initial_value => 0xffff
|
||||
uint32le :network, :initial_value => 1
|
||||
end
|
||||
|
||||
# PcapPacket describes a complete libpcap-formatted packet, which includes timestamp
|
||||
# and length information. It is used in PcapPackets class.
|
||||
class PcapPacket < BinData::MultiValue
|
||||
uint32le :ts_sec
|
||||
uint32le :ts_usec
|
||||
uint32le :incl_len, :value => lambda {data.length}
|
||||
uint32le :orig_len
|
||||
string :data, :read_length => :incl_len
|
||||
end
|
||||
|
||||
# PcapPackets is an BinData array type, used to collect packets and their associated
|
||||
# frame data. It is part of the PcapFile class.
|
||||
class PcapPackets < BinData::MultiValue
|
||||
array :data, :type => :pcap_packet, :read_until => :eof
|
||||
end
|
||||
|
||||
# PcapFile is a complete libpcap file struct, made up of a PcapHeader and PcapPackets.
|
||||
#
|
||||
# See http://wiki.wireshark.org/Development/LibpcapFileFormat
|
||||
class PcapFile < BinData::MultiValue
|
||||
pcap_header :head
|
||||
pcap_packets :body
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
|
||||
module PacketFu
|
||||
|
||||
# The Read class facilitates reading from libpcap files, which is the native file format
|
||||
# for packet capture utilities such as tcpdump, Wireshark, and PacketFu::PcapFile.
|
||||
#
|
||||
# This class requires PcapRub to be loaded (for now).
|
||||
#
|
||||
# == Example
|
||||
#
|
||||
# pkt_array = PacketFu::Read.f2a(:file => 'pcaps/my_capture.pcap')
|
||||
#
|
||||
# === file_to_array() Arguments
|
||||
#
|
||||
# :filename | :file | :out
|
||||
# The file to read from.
|
||||
#
|
||||
# == See Also
|
||||
#
|
||||
# Write, Capture
|
||||
class Read
|
||||
|
||||
# file_to_array() translates a libpcap file into an array of packets.
|
||||
def self.file_to_array(args={})
|
||||
filename = args[:filename] || args[:file] || args[:out]
|
||||
|
||||
raise ArgumentError, "Need a :filename in string form to read from." if (filename.nil? || filename.class != String)
|
||||
p = Pcap.open_offline(filename) #Using HD's patch instead of parsing it myself.
|
||||
pcap_arr = []
|
||||
while this_packet = p.next
|
||||
pcap_arr << this_packet
|
||||
end
|
||||
pcap_arr
|
||||
end
|
||||
|
||||
# f2a() is equivalent to file_to_array
|
||||
def self.f2a(args={})
|
||||
self.file_to_array(args)
|
||||
end
|
||||
|
||||
# IRB tab-completion hack.
|
||||
#--
|
||||
# This silliness is so IRB's tab-completion works for my class methods
|
||||
# when those methods are called without first instantiating. (I like
|
||||
# tab completion a lot). The alias_methods make sure they show up
|
||||
# as instance methods, but but when you call them, you're really
|
||||
# calling the class methods. Tricksy!
|
||||
def truth
|
||||
"You can't handle the truth" ; true
|
||||
end
|
||||
#:stopdoc:
|
||||
alias_method :file_to_array, :truth
|
||||
alias_method :f2a, :truth
|
||||
#:startdoc:
|
||||
|
||||
end
|
||||
end
|
|
@ -0,0 +1,362 @@
|
|||
require 'packetfu/tcpopts'
|
||||
module PacketFu
|
||||
|
||||
# Implements the Explict Congestion Notification for TCPHeader.
|
||||
#
|
||||
# ==== Header Definition
|
||||
#
|
||||
#
|
||||
# bit1 :n
|
||||
# bit1 :c
|
||||
# bit1 :e
|
||||
class TcpEcn < BinData::MultiValue
|
||||
bit1 :n
|
||||
bit1 :c
|
||||
bit1 :e
|
||||
|
||||
# Returns the TcpEcn field as an integer.
|
||||
def to_i
|
||||
(n << 2) + (c << 1) + e
|
||||
end
|
||||
end
|
||||
|
||||
# Implements flags for TCPHeader.
|
||||
#
|
||||
# ==== Header Definition
|
||||
#
|
||||
# bit1 :urg
|
||||
# bit1 :ack
|
||||
# bit1 :psh
|
||||
# bit1 :rst
|
||||
# bit1 :syn
|
||||
# bit1 :fin
|
||||
class TcpFlags < BinData::MultiValue
|
||||
bit1 :urg
|
||||
bit1 :ack
|
||||
bit1 :psh
|
||||
bit1 :rst
|
||||
bit1 :syn
|
||||
bit1 :fin
|
||||
|
||||
# Returns the TcpFlags as an integer.
|
||||
def to_i
|
||||
(urg << 5) + (ack << 4) + (psh << 3) + (rst << 2) + (syn << 1) + fin
|
||||
end
|
||||
end
|
||||
|
||||
# TCPHeader is a complete TCP struct, used in TCPPacket. Most IP traffic is TCP-based, by
|
||||
# volume.
|
||||
#
|
||||
# For more on TCP packets, see http://www.networksorcery.com/enp/protocol/tcp.htm
|
||||
#
|
||||
# ==== Header Definition
|
||||
#
|
||||
# uint16be :tcp_src, :initial_value => lambda {tcp_calc_src}
|
||||
# uint16be :tcp_dst
|
||||
# uint32be :tcp_seq, :initial_value => lambda {tcp_calc_seq}
|
||||
# uint32be :tcp_ack
|
||||
# bit4 :tcp_hlen, :initial_value => 5 # Must recalc as options are set.
|
||||
# bit3 :tcp_reserved
|
||||
# tcp_ecn :tcp_ecn
|
||||
# tcp_flags :tcp_flags
|
||||
# uint16be :tcp_win, :initial_value => 0x4000 # WinXP's default syn packet
|
||||
# uint16be :tcp_sum, :initial_value => 0 # Must set this upon generation.
|
||||
# uint16be :tcp_urg
|
||||
# string :tcp_opts
|
||||
# rest :body
|
||||
#
|
||||
# See also TcpEcn, TcpFlags, TcpOpts
|
||||
class TCPHeader < BinData::MultiValue
|
||||
|
||||
uint16be :tcp_src, :initial_value => lambda {tcp_calc_src}
|
||||
uint16be :tcp_dst
|
||||
uint32be :tcp_seq, :initial_value => lambda {tcp_calc_seq}
|
||||
uint32be :tcp_ack
|
||||
bit4 :tcp_hlen, :initial_value => 5 # Must recalc as options are set.
|
||||
bit3 :tcp_reserved
|
||||
tcp_ecn :tcp_ecn
|
||||
tcp_flags :tcp_flags
|
||||
uint16be :tcp_win, :initial_value => 0x4000 # WinXP's default syn packet
|
||||
uint16be :tcp_sum, :initial_value => 0 # Must set this upon generation.
|
||||
uint16be :tcp_urg
|
||||
string :tcp_opts
|
||||
rest :body
|
||||
|
||||
# Create a new TCPHeader object, and intialize with a random sequence number.
|
||||
def initialize(args={})
|
||||
@random_seq = rand(0xffffffff)
|
||||
@random_src = rand_port
|
||||
super
|
||||
end
|
||||
|
||||
attr_accessor :flavor
|
||||
|
||||
# tcp_calc_hlen adjusts the header length to account for tcp_opts. Note
|
||||
# that if tcp_opts does not fall on a 32-bit boundry, tcp_calc_hlen will
|
||||
# additionally pad the option string with nulls. Most stacks avoid this
|
||||
# eventuality by padding with NOP options at OS-specific points in the
|
||||
# option field. The practical effect of this is, you should tcp_calc_hlen
|
||||
# only when all the options are already set; otherwise, additional options
|
||||
# will be lost to the reciever as \x00 is an EOL option. Additionally,
|
||||
# (and this is almost certainly a bug), there is no sanity checking to
|
||||
# ensure the final tcp_opts value is 44 bytes or less (any more will bleed
|
||||
# over into the tcp payload). You are forewarned!
|
||||
#
|
||||
# If you would like to craft specifically malformed packets with
|
||||
# nonsense lengths of opts fields, you should avoid tcp_calc_hlen
|
||||
# altogether, and simply set the values for tcp_hlen and tcp_opts manually.
|
||||
def tcp_calc_hlen
|
||||
pad = (self.tcp_opts.to_s.size % 4)
|
||||
if (pad > 0)
|
||||
self.tcp_opts += ("\x00" * pad)
|
||||
end
|
||||
self.tcp_hlen = ((20 + self.tcp_opts.to_s.size) / 4)
|
||||
end
|
||||
|
||||
def tcp_calc_seq
|
||||
@random_seq
|
||||
end
|
||||
|
||||
def tcp_calc_src
|
||||
@random_src
|
||||
end
|
||||
|
||||
# Generates a random high port. This is affected by packet flavor.
|
||||
def rand_port
|
||||
rand(0xffff - 1025) + 1025
|
||||
end
|
||||
|
||||
# Returns the actual length of the TCP options.
|
||||
def tcp_opts_len
|
||||
tcp_opts.to_s.size * 4
|
||||
end
|
||||
|
||||
# Returns a more readable option list. Note, it can lack fidelity on bad option strings.
|
||||
# For more on TCP options, see the TcpOpts class.
|
||||
def tcp_options
|
||||
TcpOpts.decode(self.tcp_opts)
|
||||
end
|
||||
|
||||
# Allows a more writable version of TCP options.
|
||||
# For more on TCP options, see the TcpOpts class.
|
||||
def tcp_options=(arg)
|
||||
self.tcp_opts=TcpOpts.encode(arg)
|
||||
end
|
||||
|
||||
# Equivalent to tcp_src
|
||||
def tcp_sport
|
||||
self.tcp_src
|
||||
end
|
||||
|
||||
# Equivalent to tcp_src=
|
||||
def tcp_sport=(arg)
|
||||
self.tcp_src=(arg)
|
||||
end
|
||||
|
||||
# Equivalent to tcp_dst
|
||||
def tcp_dport
|
||||
self.tcp_dst
|
||||
end
|
||||
|
||||
# Equivalent to tcp_dst=
|
||||
def tcp_dport=(arg)
|
||||
self.tcp_dst=(arg)
|
||||
end
|
||||
|
||||
# Recalculates calculated fields for TCP (except checksum which is at the Packet level).
|
||||
def tcp_recalc(arg=:all)
|
||||
case arg
|
||||
when :tcp_hlen
|
||||
tcp_calc_hlen
|
||||
when :tcp_src
|
||||
@random_tcp_src = rand_port
|
||||
when :tcp_sport
|
||||
@random_tcp_src = rand_port
|
||||
when :tcp_seq
|
||||
@random_tcp_seq = rand(0xffffffff)
|
||||
when :all
|
||||
tcp_calc_hlen
|
||||
@random_tcp_src = rand_port
|
||||
@random_tcp_seq = rand(0xffffffff)
|
||||
else
|
||||
raise ArgumentError, "No such field `#{arg}'"
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# TCPPacket is used to construct TCP packets. They contain an EthHeader, an IPHeader, and a TCPHeader.
|
||||
#
|
||||
# == Example
|
||||
#
|
||||
# tcp_pkt = PacketFu::TCPPacket.new
|
||||
# tcp_pkt.tcp_flags.syn=1
|
||||
# tcp_pkt.tcp_dst=80
|
||||
# tcp_pkt.tcp_win=5840
|
||||
# tcp_pkt.tcp_options="mss:1460,sack.ok,ts:#{rand(0xffffffff)};0,nop,ws:7"
|
||||
#
|
||||
# tcp_pkt.ip_saddr=[rand(0xff),rand(0xff),rand(0xff),rand(0xff)].join('.')
|
||||
# tcp_pkt.ip_daddr=[rand(0xff),rand(0xff),rand(0xff),rand(0xff)].join('.')
|
||||
#
|
||||
# tcp_pkt.recalc
|
||||
# tcp_pkt.to_f('/tmp/tcp.pcap')
|
||||
#
|
||||
# == Parameters
|
||||
# :eth
|
||||
# A pre-generated EthHeader object.
|
||||
# :ip
|
||||
# A pre-generated IPHeader object.
|
||||
# :flavor
|
||||
# TODO: Sets the "flavor" of the TCP packet. This will include TCP options and the initial window
|
||||
# size, per stack. There is a lot of variety here, and it's one of the most useful methods to
|
||||
# remotely fingerprint devices. :flavor will span both ip and tcp for consistency.
|
||||
# :type
|
||||
# TODO: Set up particular types of packets (syn, psh_ack, rst, etc). This can change the initial flavor.
|
||||
# :config
|
||||
# A hash of return address details, often the output of Utils.whoami?
|
||||
class TCPPacket < Packet
|
||||
|
||||
attr_accessor :eth_header, :ip_header, :tcp_header, :headers
|
||||
|
||||
def ethernet?; true; end
|
||||
def ip?; true; end
|
||||
def tcp?; true; end
|
||||
|
||||
def initialize(args={})
|
||||
@eth_header = (args[:eth] || EthHeader.new)
|
||||
@ip_header = (args[:ip] || IPHeader.new)
|
||||
@tcp_header = (args[:tcp] || TCPHeader.new)
|
||||
@tcp_header.flavor = args[:flavor].to_s.downcase
|
||||
|
||||
@ip_header.body = @tcp_header
|
||||
@eth_header.body = @ip_header
|
||||
@headers = [@eth_header, @ip_header, @tcp_header]
|
||||
|
||||
@ip_header.ip_proto=0x06
|
||||
super
|
||||
if args[:flavor]
|
||||
tcp_calc_flavor(@tcp_header.flavor)
|
||||
else
|
||||
tcp_calc_sum
|
||||
end
|
||||
end
|
||||
|
||||
# Sets the correct flavor for TCP Packets. Recognized flavors are:
|
||||
# windows, linux, freebsd
|
||||
def tcp_calc_flavor(str)
|
||||
ts_val = Time.now.to_i + rand(0x4fffffff)
|
||||
ts_sec = rand(0xffffff)
|
||||
case @tcp_header.flavor = str.to_s.downcase
|
||||
when "windows" # WinXP's default syn
|
||||
@tcp_header.tcp_win = 0x4000
|
||||
@tcp_header.tcp_options="MSS:1460,NOP,NOP,SACK.OK"
|
||||
@tcp_header.tcp_src = rand(5000 - 1026) + 1026
|
||||
@ip_header.ip_ttl = 64
|
||||
when "linux" # Ubuntu Linux 2.6.24-19-generic default syn
|
||||
@tcp_header.tcp_win = 5840
|
||||
@tcp_header.tcp_options="MSS:1460,SACK.OK,TS:#{ts_val};0,NOP,WS:7"
|
||||
@tcp_header.tcp_src = rand(61_000 - 32_000) + 32_000
|
||||
@ip_header.ip_ttl = 64
|
||||
when "freebsd" # Freebsd
|
||||
@tcp_header.tcp_win = 0xffff
|
||||
@tcp_header.tcp_options="MSS:1460,NOP,WS:3,NOP,NOP,TS:#{ts_val};#{ts_sec},SACK.OK,EOL,EOL"
|
||||
@ip_header.ip_ttl = 64
|
||||
else
|
||||
@tcp_header.tcp_options="MSS:1460,NOP,NOP,SACK.OK"
|
||||
end
|
||||
tcp_calc_sum
|
||||
end
|
||||
|
||||
# tcp_calc_sum() computes the TCP checksum, and is called upon intialization. It usually
|
||||
# should be called just prior to dropping packets to a file or on the wire.
|
||||
#--
|
||||
# This is /not/ delegated down to @tcp_header since we need info
|
||||
# from the IP header, too.
|
||||
#++
|
||||
def tcp_calc_sum
|
||||
checksum = (ip_src.to_i >> 16)
|
||||
checksum += (ip_src.to_i & 0xffff)
|
||||
checksum += (ip_dst.to_i >> 16)
|
||||
checksum += (ip_dst.to_i & 0xffff)
|
||||
checksum += 0x06 # TCP Protocol.
|
||||
checksum += (ip_len.to_i - ((ip_hl.to_i) * 4))
|
||||
checksum += tcp_src
|
||||
checksum += tcp_dst
|
||||
checksum += (tcp_seq.to_i >> 16)
|
||||
checksum += (tcp_seq.to_i & 0xffff)
|
||||
checksum += (tcp_ack.to_i >> 16)
|
||||
checksum += (tcp_ack.to_i & 0xffff)
|
||||
checksum += ((tcp_hlen << 12) +
|
||||
(tcp_reserved << 9) +
|
||||
(tcp_ecn.to_i << 6) +
|
||||
tcp_flags.to_i
|
||||
)
|
||||
checksum += tcp_win
|
||||
checksum += tcp_urg
|
||||
|
||||
chk_tcp_opts = (tcp_opts.to_s.size % 2 == 0 ? tcp_opts.to_s : tcp_opts.to_s + "\x00")
|
||||
chk_tcp_opts.scan(/[\x00-\xff]{2}/).collect { |x| (x[0] << 8) + x[1] }.each { |y| checksum += y}
|
||||
if (ip_len - ((ip_hl + tcp_hlen) * 4)) >= 0
|
||||
real_tcp_payload = payload[0,( ip_len - ((ip_hl + tcp_hlen) * 4) )] # Can't forget those pesky FCSes!
|
||||
else
|
||||
real_tcp_payload = payload # Something's amiss here so don't bother figuring out where the real payload is.
|
||||
end
|
||||
chk_payload = (real_tcp_payload.size % 2 == 0 ? real_tcp_payload : real_tcp_payload + "\x00") # Null pad if it's odd.
|
||||
chk_payload.scan(/[\x00-\xff]{2}/).collect { |x| (x[0] << 8) + x[1] }.each { |y| checksum += y}
|
||||
checksum = checksum % 0xffff
|
||||
checksum = 0xffff - checksum
|
||||
checksum == 0 ? 0xffff : checksum
|
||||
@tcp_header.tcp_sum = checksum
|
||||
end
|
||||
|
||||
# Recalculates various fields of the TCP packet.
|
||||
#
|
||||
# ==== Parameters
|
||||
#
|
||||
# :all
|
||||
# Recomputes all calculated fields.
|
||||
# :tcp_sum
|
||||
# Recomputes the TCP checksum.
|
||||
# :tcp_hlen
|
||||
# Recomputes the TCP header length. Useful after options are added.
|
||||
def tcp_recalc(arg=:all)
|
||||
case arg
|
||||
when :tcp_sum
|
||||
tcp_calc_sum
|
||||
when :tcp_hlen
|
||||
@tcp_header.tcp_recalc :tcp_hlen
|
||||
when :all
|
||||
@tcp_header.tcp_recalc :all
|
||||
tcp_calc_sum
|
||||
else
|
||||
raise ArgumentError, "No such field `#{arg}'"
|
||||
end
|
||||
end
|
||||
|
||||
# Peek provides summary data on packet contents.
|
||||
def peek(args={})
|
||||
peek_data = ["T "]
|
||||
peek_data << "%-5d" % self.to_s.size
|
||||
peek_data << "%-21s" % "#{self.ip_saddr}:#{self.tcp_src}"
|
||||
peek_data << "->"
|
||||
peek_data << "%21s" % "#{self.ip_daddr}:#{self.tcp_dst}"
|
||||
flags = ' ['
|
||||
flags << (self.tcp_flags.urg.zero? ? "." : "U")
|
||||
flags << (self.tcp_flags.ack.zero? ? "." : "A")
|
||||
flags << (self.tcp_flags.psh.zero? ? "." : "P")
|
||||
flags << (self.tcp_flags.rst.zero? ? "." : "R")
|
||||
flags << (self.tcp_flags.syn.zero? ? "." : "S")
|
||||
flags << (self.tcp_flags.fin.zero? ? "." : "F")
|
||||
flags << '] '
|
||||
peek_data << flags
|
||||
peek_data << "S:"
|
||||
peek_data << "%08x" % self.tcp_seq
|
||||
peek_data << "|I:"
|
||||
peek_data << "%04x" % self.ip_id
|
||||
peek_data.join
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end # module PacketFu
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
|
||||
module PacketFu
|
||||
|
||||
# TcpOpts handles the translation of TCP option strings to human-readable form,
|
||||
# and vice versa. It is nearly certain to be completely rewritten, though the
|
||||
# syntax will remain the same.
|
||||
#
|
||||
# == Example
|
||||
#
|
||||
# tcp_pkt = PacketFu::TCPPacket.new
|
||||
# tcp_pkt.tcp_options="nop,nop,sack.ok,ws:7,eol"
|
||||
#
|
||||
# It's usable now, but you should not trust attacker data, since certain
|
||||
# combinations of malformed options will raise execeptions.
|
||||
#
|
||||
# See http://www.iana.org/assignments/tcp-parameters/ for the rules on sizes and such.
|
||||
class TcpOpts
|
||||
include Singleton
|
||||
|
||||
# XXX: Here be dragons!
|
||||
#
|
||||
# Like decode, encode requires fairly strict adherance to the various
|
||||
# RFCs when it comes to lengths and expected data types. If you're
|
||||
# trying to enter well-formed data, it should all work fine. If you're
|
||||
# trying to something like fuzzing, then you'll probably have better
|
||||
# luck using tcp_opts= instead.
|
||||
#
|
||||
# There are a few opportunities for attackers get get bad data passed
|
||||
# from decode over to encode to produce unexpected results (SACK
|
||||
# and timestamp manipulation seeming to be the most disasterous).
|
||||
# So don't make any life-critical decisions based on these options
|
||||
# remaining the same between the decode and encode functions.
|
||||
def self.encode(str)
|
||||
opts = str.split(/\s*,\s*/)
|
||||
binary_opts = ''
|
||||
opts.each do |opt|
|
||||
binary_opts << case opt
|
||||
when /^EOL$/i; "\x00"
|
||||
when /^NOP$/i; "\x01"
|
||||
when /^MSS\s*:/i; tcp_opts_short(2,opt)
|
||||
when /^WS\s*:/i; tcp_opts_char(3,opt)
|
||||
when /^SACK\.OK$/i; "\x04\x02"
|
||||
when /^SACK\s*:/i
|
||||
sack_opts = opt.split(/\s*:\s*/)[1].split(/\s*;\s*/).collect {|i| i.to_i}.pack("N*")
|
||||
[0x05,sack_opts.size+2,sack_opts].pack("CCa*")
|
||||
when /^ECHO\s*:/i; tcp_opts_long(6,opt)
|
||||
when /^ECHO.REPLY\s*:/i; tcp_opts_long(7,opt)
|
||||
when /^TS\s*:/i
|
||||
ts_opts = opt.split(/\s*:\s*/)[1].split(/\s*;\s*/).collect {|i| i.to_i}.pack("N*")
|
||||
[0x08,ts_opts.size+2,ts_opts].pack("CCa*")
|
||||
when /^POCP$/i; "\x09\x02"
|
||||
when /^POSP\s*:/i
|
||||
posp = [10,3,(opt.split(/\s*:\s*/)[1].to_i << 6)].pack("C3")
|
||||
when /^CC\s*:/i; tcp_opts_long(11,opt)
|
||||
when /^CC.NEW\s*:/i; tcp_opts_long(12,opt)
|
||||
when /^CC.ECHO\s*:/i; tcp_opts_long(13,opt)
|
||||
when /^ALT.CRC\s*:/i; tcp_opts_char(14,opt)
|
||||
when /^ALT.DATA\s*:/i; tcp_opts_variable(15,opt)
|
||||
when /^Skeeter\s*:/i; tcp_opts_variable(16,opt)
|
||||
when /^Bubba\s*:/i; tcp_opts_variable(17,opt)
|
||||
when /^TCO\s*:/i; tcp_opts_char(18,opt)
|
||||
when /^MD5\s*:/i; tcp_opts_variable(19,opt)
|
||||
when /^QSR\s*:/i; tcp_opts_variable(27,opt) # TODO: Do the bitwise math to match the decode.
|
||||
when /^0x[0-9a-f][0-9a-f]\s*:/i; tcp_opts_variable(opt[0,4].to_i(16),opt)
|
||||
else
|
||||
raise ArgumentError, "Invalid tcp_options format or entry. Perhaps you want tcp_opts?"
|
||||
end
|
||||
end
|
||||
binary_opts
|
||||
end
|
||||
|
||||
def self.tcp_opts_variable(optnum,optstr)
|
||||
ret = [optnum,0,optstr.split(/\s*:\s*/)[1]]
|
||||
ret[1] = ret.pack("CCH*").size
|
||||
ret.pack("CCH*")
|
||||
end
|
||||
|
||||
def self.tcp_opts_char(optnum,optstr)
|
||||
[optnum,3,optstr.split(/\s*:\s*/)[1][0,3].to_i].pack("C3")
|
||||
end
|
||||
|
||||
def self.tcp_opts_short(optnum,optstr)
|
||||
[optnum,4,optstr.split(/\s*:\s*/)[1].to_i].pack("CCn")
|
||||
end
|
||||
|
||||
def self.tcp_opts_long(optnum,optstr)
|
||||
[optnum,6,optstr.split(/\s*:\s*/)[1].to_i].pack("CCN")
|
||||
end
|
||||
|
||||
|
||||
# XXX: Here be dragons!
|
||||
#
|
||||
# There are a few opportunities for attackers get get bad data passed
|
||||
# from decode over to encode to produce unexpected results (SACK
|
||||
# and timestamp manipulation seeming to be the most disasterous).
|
||||
# So don't make any life-critical decisions based on these options
|
||||
# remaining the same between the decode and encode functions.
|
||||
def self.decode(str)
|
||||
bare_opts = []
|
||||
invalid_opts = false
|
||||
invalid_opts = true if str.size > 44
|
||||
opts = StringIO.new(str)
|
||||
while opts.pos < opts.size
|
||||
bare_opts << case opts.read(1)
|
||||
when "\x00"; "\x00"
|
||||
when "\x01"; "\x01"
|
||||
else
|
||||
arr = []
|
||||
opts.seek(opts.pos-1,0) # No StringIO.read(-1)? Lame.
|
||||
arr << opts.read(1)
|
||||
sz = opts.read(1)
|
||||
if sz.nil? # Every option needs a size.
|
||||
invalid_opts = true
|
||||
else
|
||||
sz = sz.unpack("C")[0]
|
||||
if sz <= 1 # Every option's size is 2 or greater.
|
||||
invalid_opts = true
|
||||
else
|
||||
arr << sz
|
||||
arr << opts.read(sz-2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if invalid_opts
|
||||
"INVALID:#{str}"
|
||||
else
|
||||
TcpOpts.translate(bare_opts)
|
||||
end
|
||||
end
|
||||
|
||||
# Get an array of TCP options as produced by decode() and translate it into
|
||||
# something passing for human readable.
|
||||
def self.translate(arr)
|
||||
translated_opts = arr.collect do |opt|
|
||||
case opt
|
||||
when "\x00"; "EOL"
|
||||
when "\x01"; "NOP"
|
||||
else
|
||||
if opt[2].class == String
|
||||
TcpOpts.option_to_s(opt[0].unpack("C")[0],opt[2])
|
||||
else
|
||||
"INVALID:#{opt.pack("aC")}"
|
||||
end
|
||||
end
|
||||
end
|
||||
translated_opts.join(",")
|
||||
end
|
||||
|
||||
# Option_to_s translates TCP option strings into a human-readable
|
||||
# form (really, a nerd-readable form, and only if that nerd has
|
||||
# a copy of the various RFC's handy). It makes big assumptions
|
||||
# that the sizes are RFC correct by this point, what with the
|
||||
# unpacks and other presentations; for example, SACK-OK options
|
||||
# are always two bytes, and no payload, so if you get a SACK-OK
|
||||
# with a payload, it will be invisible when you process it with
|
||||
# option_to_s.
|
||||
#
|
||||
# Underruns and other argument errors /should/ be impossible.
|
||||
# If you find one, please file a bug!
|
||||
#
|
||||
# If you require more precision than this (eg, to
|
||||
# account for malformed options), you should probably do
|
||||
# your own opts processing using the bare tcp_opts values.
|
||||
#
|
||||
# Eventually, option_to_s should get a lot smarter about
|
||||
# weirdly-formed options. And it at least shouldn't raise
|
||||
# exceptions.
|
||||
def self.option_to_s(optnum,value)
|
||||
|
||||
case optnum
|
||||
when 2; "MSS:#{value.unpack("n")}" # Max Segment Size
|
||||
when 3; "WS:#{value.unpack("C")}" # Window Scale
|
||||
when 4; "SACK.OK" # SACK Permitted
|
||||
when 5; sack_opt = "SACK:" # SACK values
|
||||
if value.size % 4 == 0 # Well formed or not.
|
||||
edges = value.scan(/[\x00-\xff]{4}/).collect {|h| h.unpack("N")}.join(';')
|
||||
else
|
||||
edges = value.unpack("H*")[0]
|
||||
end
|
||||
sack_opt + edges
|
||||
when 6; "ECHO:#{value.unpack("N")}" # Echo
|
||||
when 7; "ECHO.REPLY:#{value.unpack("N")}" # Echo Reply
|
||||
when 8; ts_opt = "TS:" # Timestamp and TS-echo reply
|
||||
ts_opt << value[0,4].unpack("N")[0].to_s
|
||||
ts_opt << ";"
|
||||
ts_opt << value[4,4].unpack("N")[0].to_s
|
||||
when 9; "POCP" # Partial Order Connection Permitted
|
||||
when 10; "POSP:" + # Partial Order Service Profile
|
||||
"%02d" % ((value.unpack("C")[0] >> 6).to_s(2)) # Partial Order bits
|
||||
when 11; "CC:#{value.unpack("N")}" # Connection Count. RFC 1644 is hi-larious, btw.
|
||||
when 12; "CC.NEW:#{value.unpack("N")}" # Connection Count New.
|
||||
when 13; "CC.EHCO:#{value.unpack("N")}" # Conn. Count Echo.
|
||||
when 14: "ALT.CRC:#{value.unpack("C")}" # Alt Checksum request
|
||||
when 15: "ALT.DATA:#{value.unpack("H*")}" # Alt checksum data. I'm too dumb for this.
|
||||
when 16: "Skeeter:#{value.unpack("H*")}" # Skeeter crypto.
|
||||
when 17: "Bubba:#{value.unpack("H*")}" # Bubba crypto.
|
||||
when 18: "TCO:#{value.unpack("C")}" # Trailer Checksum Option. Nobody knows what this is.
|
||||
when 19: "MD5:#{value.unpack("H*")}" # MD5 Signature Option. Hash-signed TCP? Outrageous!
|
||||
when 27: qsr_opt = "QSR:" # Quick-Start Request. Experimental in Jan 2007.
|
||||
qsr_val = []
|
||||
qsr_val << (value[0,1].unpack("C")[0] >> 4)
|
||||
qsr_val << (value[0,1].unpack("C")[0] & 0x0f)
|
||||
qsr_val << value[1,1].unpack("C")[0]
|
||||
qsr_val << value[2,4].unpack("N")[0] # Note bits 30,31 RFC-SHOULD be zero.
|
||||
qsr_opt + qsr_val.join(';')
|
||||
# Pretty much everything else is obsolete, experiemental, unused, or undoc'ed.
|
||||
else; "0x#{[optnum].pack("C").unpack("H*")[0].upcase}:#{value.unpack("H*")}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
|
||||
module PacketFu
|
||||
|
||||
# UDPHeader is a complete UDP struct, used in UDPPacket. Many Internet-critical protocols
|
||||
# rely on UDP, such as DNS and World of Warcraft.
|
||||
#
|
||||
# For more on UDP packets, see http://www.networksorcery.com/enp/protocol/udp.htm
|
||||
#
|
||||
# ==== Header Definition
|
||||
# uint16be :udp_src
|
||||
# uint16be :udp_dst
|
||||
# uint16be :udp_len, :initial_value => lambda {udp_calc_len}
|
||||
# uint16be :udp_sum, :initial_value => 0 # Checksum off
|
||||
# rest :body
|
||||
class UDPHeader < BinData::MultiValue
|
||||
uint16be :udp_src
|
||||
uint16be :udp_dst
|
||||
uint16be :udp_len, :initial_value => lambda {udp_calc_len}
|
||||
uint16be :udp_sum, :initial_value => 0 # Checksum off
|
||||
rest :body
|
||||
|
||||
# Returns the true length of the UDP packet.
|
||||
def udp_calc_len
|
||||
body.to_s.size + 8
|
||||
end
|
||||
|
||||
# Recalculates calculated fields for UDP.
|
||||
def udp_recalc(args=:all)
|
||||
case args
|
||||
when :udp_len
|
||||
self.udp_len = udp_calc_len
|
||||
when :all
|
||||
self.udp_recalc(:udp_len)
|
||||
else
|
||||
raise ArgumentError, "No such field `#{arg}'"
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# UDPPacket is used to construct UDP Packets. They contain an EthHeader, an IPHeader, and a UDPHeader.
|
||||
#
|
||||
# == Example
|
||||
#
|
||||
# udp_pkt = PacketFu::UDPPacket.new
|
||||
# udp_pkt.udp_src=rand(0xffff-1024) + 1024
|
||||
# udp_pkt.udp_dst=53
|
||||
#
|
||||
# udp_pkt.ip_saddr="1.2.3.4"
|
||||
# udp_pkt.ip_daddr="10.20.30.40"
|
||||
#
|
||||
# udp_pkt.recalc
|
||||
# udp_pkt.to_f('/tmp/udp.pcap')
|
||||
#
|
||||
# == Parameters
|
||||
#
|
||||
# :eth
|
||||
# A pre-generated EthHeader object.
|
||||
# :ip
|
||||
# A pre-generated IPHeader object.
|
||||
# :flavor
|
||||
# TODO: Sets the "flavor" of the UDP packet. UDP packets don't tend have a lot of
|
||||
# flavor, but their underlying ip headers do.
|
||||
# :config
|
||||
# A hash of return address details, often the output of Utils.whoami?
|
||||
class UDPPacket < Packet
|
||||
|
||||
attr_accessor :eth_header, :ip_header, :udp_header
|
||||
|
||||
def ethernet?; true; end
|
||||
def ip?; true; end
|
||||
def udp?; true; end
|
||||
|
||||
def initialize(args={})
|
||||
@eth_header = (args[:eth] || EthHeader.new)
|
||||
@ip_header = (args[:ip] || IPHeader.new)
|
||||
@udp_header = (args[:udp] || UDPHeader.new)
|
||||
|
||||
@ip_header.body = @udp_header
|
||||
@eth_header.body = @ip_header
|
||||
@headers = [@eth_header, @ip_header, @udp_header]
|
||||
|
||||
@ip_header.ip_proto=0x11
|
||||
super
|
||||
udp_calc_sum
|
||||
end
|
||||
|
||||
# udp_calc_sum() computes the TCP checksum, and is called upon intialization.
|
||||
# It usually should be called just prior to dropping packets to a file or on the wire.
|
||||
def udp_calc_sum
|
||||
# This is /not/ delegated down to @udp_header since we need info
|
||||
# from the IP header, too.
|
||||
checksum = (ip_src.to_i >> 16)
|
||||
checksum += (ip_src.to_i & 0xffff)
|
||||
checksum += (ip_dst.to_i >> 16)
|
||||
checksum += (ip_dst.to_i & 0xffff)
|
||||
checksum += 0x11
|
||||
checksum += udp_len
|
||||
checksum += udp_src
|
||||
checksum += udp_dst
|
||||
checksum += udp_len
|
||||
if udp_len >= 8
|
||||
real_udp_payload = payload[0,(udp_len-8)] # For IP trailers. This isn't very reliable, though. :/
|
||||
else
|
||||
real_udp_payload = payload # I'm not going to mess with this right now.
|
||||
end
|
||||
chk_payload = (real_udp_payload.size % 2 == 0 ? real_udp_payload : real_udp_payload + "\x00")
|
||||
chk_payload.scan(/[\x00-\xff]{2}/).collect { |x| (x[0] << 8) + x[1] }.each { |y| checksum += y}
|
||||
checksum = checksum % 0xffff
|
||||
checksum = 0xffff - checksum
|
||||
checksum == 0 ? 0xffff : checksum
|
||||
@udp_header.udp_sum = checksum
|
||||
end
|
||||
|
||||
# udp_recalc() recalculates various fields of the TCP packet. Valid arguments are:
|
||||
#
|
||||
# :all
|
||||
# Recomputes all calculated fields.
|
||||
# :udp_sum
|
||||
# Recomputes the UDP checksum.
|
||||
# :udp_len
|
||||
# Recomputes the UDP length.
|
||||
def udp_recalc(args=:all)
|
||||
case args
|
||||
when :udp_len
|
||||
@udp_header.udp_recalc
|
||||
when :udp_sum
|
||||
udp_calc_sum
|
||||
when :all
|
||||
@udp_header.udp_recalc
|
||||
udp_calc_sum
|
||||
else
|
||||
raise ArgumentError, "No such field `#{arg}'"
|
||||
end
|
||||
end
|
||||
|
||||
# Peek provides summary data on packet contents.
|
||||
def peek(args={})
|
||||
peek_data = ["U "]
|
||||
peek_data << "%-5d" % self.to_s.size
|
||||
peek_data << "%-21s" % "#{self.ip_saddr}:#{self.udp_src}"
|
||||
peek_data << "->"
|
||||
peek_data << "%21s" % "#{self.ip_daddr}:#{self.udp_dst}"
|
||||
peek_data << "%23s" % "I:"
|
||||
peek_data << "%04x" % self.ip_id
|
||||
peek_data.join
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end # module PacketFu
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
require 'singleton'
|
||||
module PacketFu
|
||||
|
||||
# Utils is a collection of various and sundry network utilities that are useful for packet
|
||||
# manipulation.
|
||||
class Utils
|
||||
include Singleton
|
||||
|
||||
# Returns the MAC address of an IP address, or nil if it's not responsive to arp. Takes
|
||||
# a dotted-octect notation of the target IP address, as well as a number of parameters:
|
||||
#
|
||||
# === Parameters
|
||||
# :eth_saddr
|
||||
# Source MAC address. Defaults to "00:00:00:00:00:00".
|
||||
# :ip_saddr
|
||||
# Source IP address. Defaults to "0.0.0.0"
|
||||
# :flavor
|
||||
# The flavor of the ARP request. Defaults to :none.
|
||||
# :timeout
|
||||
# Timeout in seconds. Defaults to 3.
|
||||
#
|
||||
# === Example
|
||||
# PacketFu::Utils::arp("192.168.1.1") #=> "00:18:39:01:33:70"
|
||||
# PacketFu::Utils::arp("192.168.1.1", :timeout => 5, :flavor => :hp_deskjet)
|
||||
#
|
||||
# === Warning
|
||||
#
|
||||
# It goes without saying, spewing forged ARP packets on your network is a great way to really
|
||||
# irritate your co-workers.
|
||||
def self.arp(target_ip,args={})
|
||||
arp_pkt = PacketFu::ARPPacket.new(:flavor => (args[:flavor] || :none))
|
||||
arp_pkt.eth_saddr = arp_pkt.arp_saddr_mac = (args[:eth_saddr] || ($packetfu_default.config[:eth_saddr] if $packetfu_default) || "00:00:00:00:00:00" )
|
||||
arp_pkt.eth_daddr = "ff:ff:ff:ff:ff:ff"
|
||||
arp_pkt.arp_daddr_mac = "00:00:00:00:00:00"
|
||||
arp_pkt.arp_saddr_ip = (args[:ip_saddr] || ($packetfu_default.config[:ip_saddr] if $packetfu_default) || "0.0.0.0")
|
||||
arp_pkt.arp_daddr_ip = target_ip
|
||||
iface = (args[:iface] || ($packetfu_default.iface if $packetfu_default) || "eth0")
|
||||
# Stick the Capture object in its own thread.
|
||||
cap_thread = Thread.new do
|
||||
target_mac = nil
|
||||
cap = PacketFu::Capture.new(:iface => iface, :start => true,
|
||||
:filter => "arp src #{target_ip} and ether dst #{arp_pkt.eth_saddr}")
|
||||
arp_pkt.to_w(iface) # Shorthand for sending single packets to the default interface.
|
||||
timeout = 0
|
||||
while target_mac.nil? && timeout <= (args[:timeout] || 3)
|
||||
if cap.save > 0
|
||||
arp_response = PacketFu::Packet.parse(cap.array[0])
|
||||
target_mac = arp_response.arp_saddr_mac if arp_response.arp_saddr_ip = target_ip
|
||||
end
|
||||
timeout += 0.1
|
||||
sleep 0.1 # Check for a response ten times per second.
|
||||
end
|
||||
target_mac
|
||||
end # cap_thread
|
||||
cap_thread.value
|
||||
end # def self.arp
|
||||
|
||||
# Discovers the local IP and Ethernet address, which is useful for writing
|
||||
# packets you expect to get a response to. Note, this is a noisy
|
||||
# operation; a UDP packet is generated and dropped on to the default (or named)
|
||||
# interface, and then captured (which means you need to be root to do this).
|
||||
#
|
||||
# whoami? returns a hash of :eth_saddr, :eth_src, :ip_saddr, :ip_src,
|
||||
# :eth_dst, and :eth_daddr (the last two are usually suitable for a
|
||||
# gateway mac address). It's most useful as an argument to PacketFu::Config.new.
|
||||
#
|
||||
# === Parameters
|
||||
# :iface => "eth0"
|
||||
# An interface to listen for packets on. Note that since we rely on the OS to send the probe packet,
|
||||
# you will need to specify a target which will use this interface.
|
||||
# :target => "1.2.3.4"
|
||||
# A target IP address. By default, a packet will be sent to a random address in the 177/8 network.
|
||||
# Since this network is IANA reserved (for now), this network should be handled by your default gateway
|
||||
# and default interface.
|
||||
def self.whoami?(args={})
|
||||
if args[:iface] =~ /^lo/ # Linux loopback more or less. Need a switch for windows loopback, too.
|
||||
dst_host = "127.0.0.1"
|
||||
else
|
||||
dst_host = (args[:target] || IPAddr.new((rand(16777216) + 2969567232), Socket::AF_INET).to_s)
|
||||
end
|
||||
|
||||
dst_port = rand(0xffff-1024)+1024
|
||||
msg = "PacketFu whoami? packet #{(Time.now.to_i + rand(0xffffff)+1)}"
|
||||
cap = Capture.new(:iface => (args[:iface] || Pcap.lookupdev), :start => true, :filter => "udp and dst host #{dst_host} and dst port #{dst_port}")
|
||||
UDPSocket.open.send(msg,0,dst_host,dst_port)
|
||||
cap.save
|
||||
pkt = Packet.parse(cap.array[0]) unless cap.save.zero?
|
||||
timeout = 0
|
||||
while timeout < 1 # Sometimes packet generation can be a little pokey.
|
||||
if pkt
|
||||
timeout = 1.1 # Cancel the timeout
|
||||
if pkt.payload == msg
|
||||
my_data = {
|
||||
:iface => args[:iface] || Pcap.lookupdev || 'lo',
|
||||
:pcapfile => args[:pcapfile] || "/tmp/out.pcap",
|
||||
:eth_saddr => pkt.eth_saddr,
|
||||
:eth_src => pkt.eth_src.to_s,
|
||||
:ip_saddr => pkt.ip_saddr,
|
||||
:ip_src => pkt.ip_src.to_s,
|
||||
:eth_dst => pkt.eth_dst.to_s,
|
||||
:eth_daddr => pkt.eth_daddr
|
||||
}
|
||||
else raise SecurityError,
|
||||
"whoami() packet doesn't match sent data. Something fishy's going on."
|
||||
end
|
||||
else
|
||||
sleep 0.1; timeout += 0.1
|
||||
cap.save
|
||||
pkt = Packet.parse(cap.array[0]) unless cap.save.zero?
|
||||
end
|
||||
raise SocketError, "Didn't recieve the whomi() packet." if !pkt
|
||||
cap = nil
|
||||
end
|
||||
my_data
|
||||
end
|
||||
|
||||
# This is a brute-force approach at trying to find a suitable interface with an IP address.
|
||||
def self.lookupdev
|
||||
# XXX cycle through eth0-9 and wlan0-9, and if a cap start throws a RuntimeErorr (and we're
|
||||
# root), it's not a good interface. Boy, really ought to fix lookupdev directly with another
|
||||
# method that returns an array rather than just the first candidate.
|
||||
end
|
||||
|
||||
|
||||
end # class Utils
|
||||
|
||||
end # module PacketFu
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
|
||||
module PacketFu
|
||||
|
||||
# The Write class facilitates writing to libpcap files, which is the native file format
|
||||
# for packet capture utilities such as tcpdump, Wireshark, and PacketFu::PcapFile.
|
||||
#
|
||||
# == Example
|
||||
#
|
||||
# cap = PacketFu::Capture.new(:start => true)
|
||||
# sleep 10
|
||||
# cap.save
|
||||
# pkt_array = cap.array
|
||||
# PacketFu::Write.a2f(:file => 'pcaps/my_capture.pcap', :array => pkt_array)
|
||||
#
|
||||
# === array_to_file() Arguments
|
||||
#
|
||||
# :filename | :file | :out
|
||||
# The file to write to. If it exists, it will be overwritten. By default, no file will be written.
|
||||
#
|
||||
# :array | arr
|
||||
# The array to read packet data from. Note, these should be strings, and not packet objects!
|
||||
#
|
||||
# :ts | :timestamp
|
||||
# The starting timestamp. By default, it is the result of Time.now.to_i
|
||||
#
|
||||
# :ts_inc | :timestamp_increment
|
||||
# The timestamp increment, in seconds. (Sorry, no usecs yet)
|
||||
#
|
||||
# == See Also
|
||||
#
|
||||
# Read, Capture
|
||||
class Write
|
||||
|
||||
# Writes an array of binary data to a libpcap file.
|
||||
def self.array_to_file(args={})
|
||||
filename = args[:filename] || args[:file] || args[:out] || :nowrite
|
||||
arr = args[:arr] || args[:array] || []
|
||||
ts = args[:ts] || args[:timestamp] || Time.now.to_i
|
||||
ts_inc = args[:ts_inc] || args[:timestamp_increment] || 1
|
||||
|
||||
if arr.class != Array
|
||||
raise ArgumentError, "This needs to be an array."
|
||||
end
|
||||
|
||||
formatted_packets = []
|
||||
arr.each do |pkt|
|
||||
this_pkt = PcapPacket.new
|
||||
this_pkt.data = pkt[0,0xffff]
|
||||
this_pkt.orig_len = pkt.size # orig_len isn't calc'ed already.
|
||||
this_pkt.ts_sec = ts += decimal_to_usecs(ts_inc)[0]
|
||||
formatted_packets << this_pkt.to_s
|
||||
end
|
||||
filedata = PcapFile.new
|
||||
filedata.read(PcapFile.new.to_s + formatted_packets.join) # Like a cat playing the bass.
|
||||
|
||||
if filename != :nowrite
|
||||
out = File.new(filename.to_s, 'w')
|
||||
out.print filedata
|
||||
out.close
|
||||
# Return [filename, file size, # of packets, initial timestamp, timestamp increment]
|
||||
ret = [filename,filedata.to_s.size,arr.size,ts,ts_inc]
|
||||
else
|
||||
ret = [nil,filedata.to_s.size,arr.size,ts,ts_inc]
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# A synonym for array_to_file()
|
||||
def self.a2f(args={})
|
||||
self.array_to_file(args)
|
||||
end
|
||||
|
||||
# TODO: Wire this in later to enable incrementing by microseconds.
|
||||
def self.decimal_to_usecs(decimal)
|
||||
secs = decimal.to_i
|
||||
usecs = decimal.to_f.to_s.split('.')[1]
|
||||
[secs,usecs]
|
||||
end
|
||||
|
||||
# IRB tab-completion hack.
|
||||
def truth
|
||||
"Stranger than fiction" ; true
|
||||
end
|
||||
#:stopdoc:
|
||||
alias_method :array_to_file, :truth
|
||||
alias_method :a2f, :truth
|
||||
#:startdoc:
|
||||
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue