4 // Fraunhofer Institute for Open Communication Systems (FOKUS)
5 // Competence Center NETwork research (NET), St. Augustin, GERMANY
6 // Stefan Bund <g0dil@berlios.de>
8 // This program is free software; you can redistribute it and/or modify
9 // it under the terms of the GNU General Public License as published by
10 // the Free Software Foundation; either version 2 of the License, or
11 // (at your option) any later version.
13 // This program is distributed in the hope that it will be useful,
14 // but WITHOUT ANY WARRANTY; without even the implied warranty of
15 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 // GNU General Public License for more details.
18 // You should have received a copy of the GNU General Public License
19 // along with this program; if not, write to the
20 // Free Software Foundation, Inc.,
21 // 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
23 /** \mainpage The SENF Packet Library
25 The SENF Packet library provides facilities to analyze, manipulate and create structured packet
26 oriented data (e.g. network packets).
31 \section packet_intro_arch Overall Architecture
33 The Packet library consists of several components:
35 \li The \ref packet_module manages the packet data and provides the framework for handling the
36 chain of packet headers. The visible interface is provided by the Packet class.
37 \li \ref packetparser provides the framework for interpreting packet data. It handles
38 parsing the packet information into meaningful values.
39 \li The \ref protocolbundles provide concrete implementations for interpreting packets of
40 some protocol. The Protocol Bundles are built on top of the basic packet library.
42 All these components work together to provide a hopefully simple and intuitive interface to
43 packet parsing and creation.
48 \section packet_intro_usage Using the packet library
50 This chapter discusses the usage of the packet library from a high level view.
52 \see \ref packet_usage
55 \section packet_intro_parser Parsing packet data
57 This chapter goes into more detail discussing the usage of packet parsers.
59 \li categorizing packet parsers
60 \li reading and writing values
61 \li using complex parsers
63 \see \ref packetparser
66 \section protocolbundles Supported packet types (protocols)
68 Each protocol bundle provides a collection of related concrete packet classes for a group of
71 \li \ref protocolbundle_default : Some basic default protocols: Ethernet, Ip, TCP, UDP
72 \li \ref protocolbundle_mpegdvb : MPEG and DVB protocols
74 There are two ways to link with a bundle
76 \li If you only work with known packets which you explicitly reference you may just link with
77 the corresponding library.
78 \li If you need to parse unknown packets and want those to be parsed as complete as possible
79 without explicitly referencing the packet type, you will need to link against the combined
80 object file built for every bundle. This way, all packets defined in the bundle will be
81 included whether they are explicitly referenced or not (and they will all automatically be
85 \section packet_intro_new Defining new packet types
87 The packet library provides the framework which allows to define arbitrary packet types. There
88 is quite some information needed to completely specify a specific type of paceket.
93 /** \page packet_arch Overall Packet library Architecture
95 The packet library handles network packets of a large number of protocols. We work with a packet
101 \section packet_arch_handle The Packet handle
103 Whenever we are using a Packet, we are talking about a senf::Packet (or a
104 senf::ConcretePacket). This class is a \e handle referencing an internally managed packet data
105 structure. So even though we pass senf::Packet instances around by value, they work like
106 references. The packet library automatically manages all required memory resources using
109 Different Packet handles may really internally share one Packet data structure if they both
110 point to the same packet.
113 \section packet_arch_data The Packet as a 'bunch of bytes'
115 From the outside, a packet is just a bunch of bytes just as it is read from (or will be
116 written to) the wire. At this low-level view, we can access the data in it's raw form but
117 have no further information about what kind of packet we have.
119 The packet library provides a consistent container interface for this representation.
124 // Change first byte of packet to 0
127 // Copy packet data into a vector
128 std::vector<char> data (p.data().size());
129 std::copy(p.data().begin(), p.data().end(), data.begin());
132 This type of access is primarily needed when reading or writing packets (e.g. to/from the
135 \see senf::Packet::data() \n
139 \section packet_arch_chain The Interpreter Chain
141 On the next level, the packet is divided into a nested list of sub-packets (or headers) called
142 interpreters. Each senf::Packet handle internally points to an interpreter or header. This
143 allows us to access one and the same packet in different ways.
145 Consider an Ethernet Packet with an IP payload holding a UDP packet. We may reference either the
146 Ethernet packet as a whole or we may reference the IP or UDP interpreters (sub-packets or
147 headers). All handles really refer to the \e same data structure but provide access to a
148 different (sub-)range of the data in the packet.
150 We can navigate around this chained structure using appropriate members:
153 // eth, ip and udp all reference the same internal packet data albeit at different data ranges
155 Packet ip = eth.next();
156 Packet udp = ip.next();
158 eth.next() == ip // true
159 eth.next().is<IPv4Packet>() // true
160 eth.next().next() == udp // true
161 eth.next().is<UDPPacket>() // false
162 eth.find<UDPPacket>() == udp // true
164 udp.find<EthernetPacket>() // throws InvalidPacketChainException
165 udp.find<EthernetPacket>(senf::nothrow) // An in-valid() senf::Packet which tests as 'false'
166 udp.find<UDPPacket> == udp // true
167 udp.first<IPv4Packet>() // throws InvalidPacketChainException
169 udp.prev() == ip // true
170 udp.prev<EthernetPacket>() // throws Inv
173 \see \ref packet_module
176 \section packet_arch_parser Parsing specific Protocols
178 On the next level, the packet library allows us to parse the individual protocols. This gives us
179 access to the protocol specific data members of a packet and allows us to access or manipulate a
180 packet in a protocol specific way.
182 To access this information, we need to use a protocol specific handle, the senf::ConcretePacket
183 which takes as a template argument the specific type of packet to be interpreted. This allows us
184 to easily interpret or create packets. Here an example on how to create a new Etheret / IP / UDP
185 / Payload packet interpreter chain:
188 // EthernetPacket, IPv4Packet, UDPPacket and DataPacket are typedefs for corresponding
189 // ConcretePacket instantiations
190 senf::EthernetPacket eth (senf::EthernetPacket::create());
191 senf::IPv4Packet ip (senf::IPv4Packet ::createAfter(eth));
192 senf::UDPPacket udp (senf::UDPPacket ::createAfter(ip));
193 senf::DataPacket payload (senf::DataPacket ::createAfter(udp,
194 std::string("Hello, world!")));
196 udp->source() = 2000u;
197 udp->destination() = 2001u;
199 ip->source() = senf::INet4Address::from_string("192.168.0.1");
200 ip->destination() = senf::INet4Address::from_string("192.168.0.2");
201 eth->source() = senf::MACAddress::from_string("00:11:22:33:44:55");
202 eth->destination() = senf::MACAddress::from_string("00:11:22:33:44:66");
207 Again, realize, that \a eth, \a ip, \a udp and \a payload share the same internal packet
208 data structure (the respective \c data() members all provide access to the same underlying
209 container however at different byte ranges): The complete packet can be accessed at
210 <tt>eth.data()</tt> whereas <tt>payload.data()</tt> only holds UDP payload (in this case the
211 string "Hello, world!").
213 \see \ref packetparser \n
217 /** \page packet_usage Using the packet library
221 \section packet_usage_intro Includes
223 To use the library, you need to include the appropriate header files. This will probably happen
224 automatically when including the specific protocol headers. If needed, you may explicitly use
227 #include "Packets.hh"
232 \warning Never include any other Packets library header directly, only include \c
233 Packets.hh or one (or several) protocol headers from the protocol bundles.
235 Most every use of the packet library starts with some concrete packet typedef. Some fundamental
236 packet types are provided by \ref protocolbundle_default.
239 \section packet_usage_create Creating a new packet
241 Building on those packet types, this example will build a complex packet: This will be an
242 Ethernet packet containing an IPv4 UDP packet. We begin by building the raw packet skeleton:
245 #include "Packets/DefaultBundle/EthernetPacket.hh"
246 #include "Packets/DefaultBundle/IPv4Packet.hh"
247 #include "Packets/DefaultBundle/UDPPacket.hh"
249 senf::EthernetPacket eth (senf::EthernetPacket::create());
250 senf::IPv4Packet ip (senf::IPv4Packet ::createAfter(eth));
251 senf::UDPPacket udp (senf::UDPPacket ::createAfter(ip));
252 senf::DataPacket payload (senf::DataPacket ::createAfter(udp,
253 std::string("Hello, world!")));
256 These commands create what is called an interpreter chain. This chain consists of four
257 interpreters. All interpreters reference the same data storage. This data storage is a random
258 access sequence which contains the data bytes of the packet.
260 \note The data structures allocated are automatically managed using reference counting. In this
261 example we have four packet references each referencing the same underlying data
262 structure. This data structure will be freed when the last reference to it goes out of
265 The packet created above already has the correct UDP payload (The string "Hello, world!")
266 however all protocol fields are empty. We need to set those protocol fields:
269 udp->source() = 2000u;
270 udp->destination() = 2001u;
272 ip->source() = senf::INet4Address::from_string("192.168.0.1");
273 ip->destination() = senf::INet4Address::from_string("192.168.0.2");
274 eth->source() = senf::MACAddress::from_string("00:11:22:33:44:55");
275 eth->destination() = senf::MACAddress::from_string("00:11:22:33:44:66");
280 As seen above, packet fields are accessed using the <tt>-></tt> operator whereas other packet
281 facilities (like \c finalize()) are directly accessed using the member operator. The field
282 values are simply set using appropriately named accessors. As a last step, the \c finalize()
283 call will update all calculated fields (fields like next-protocol, header or payload length,
284 checksums etc). Now the packet is ready. We may now send it out using a packet socket
287 senf::PacketSocketHandle sock ("eth0");
288 sock.write(eth.data());
292 \section packet_usage_read Reading and parsing packets
294 The chain navigation functions are also used to parse a packet. Let's read an Ethernet packet
295 from a packet socket handle:
298 senf::PacketSocketHandle sock ("eth0");
299 senf::EthernetPacket packet (senf::EthernetPacket::create(senf::noinit));
300 sock.read(packet.data(),0u);
303 This first creates an uninitialized Ethernet packet and then reads into this packet. We can now
304 parse this packet. Let's find out, whether this is a UDP packet destined to port 2001:
308 senf::UDPPacket udp (packet.find<UDPPacket>());
309 if (udp->destination() == 2001u) {
312 } catch (senf::TruncatedPacketException &) {
313 std::cerr << "Ooops !! Broken packet received\n";
314 } catch (senf::InvalidPacketChainException &) {
315 std::cerr << "Not a udp packet\n";
319 TruncatedPacketException is thrown by <tt>udp->destination()</tt> if that field cannot be
320 accessed (that is it would be beyond the data read which means we have read a truncated
321 packet). More generally, whenever a field cannot be accessed because it would be out of bounds
322 of the data read, this exception is generated.
325 \section packet_usage_container The raw data container
327 Every packet is based internally on a raw data container holding the packet data. This container
328 is accessed via senf::Packet::data() member.
330 This container is a random access container. It can be used like an ordinary STL container and
331 supports all the standard container members.
336 // Insert 5 0x01 bytes
337 p.data().insert(p.data().begin()+5, 5, 0x01);
339 // Insert data from another container
340 p.data().insert(p.data().end(), other.begin(), other.end());
342 // Erase a single byte
343 p.data().erase(p.data().begin()+5);
345 // XOR byte 5 with 0xAA
349 A packet consists of a list of interpreters (packet headers or protocols) which all reference
350 the same data container at different byte ranges. Each packet consists of the protocol header \e
351 plus the packets payload. This means, that the data container ranges of successive packets from
352 a single interpreter chain are nested.
354 Example: The packet created above (the Ethernet-IP-UDP packet with payload "Hello, world!") has
355 4 Interpreters: Ethernet, IPv4, UDP and the UDP payload data. The nested data containers lead to
356 the following structure
359 // The ethernet header has a size of 14 bytes
360 eth.data().begin() + 14 == ip.data().begin()
361 eth.data().end() == ip.data().end()
363 // The IP header has a size of 20 bytes and therefore
364 ip.data().begin() + 20 == udp.data().begin()
365 ip.data().end() == udp.data().end()
367 // The UDP header has a size of 8 bytes and thus
368 udp.data().begin() + 8 == payload.data().begin()
369 udp.data().end() == payload.data().end()
372 This nesting will (and must) always hold: The data range of a subsequent packet will always be
373 within the range of it's preceding packet.
375 \warning It is forbidden to change the data of a subsequent packet interpreter from the
376 preceding packet even if the data container includes this data. If you do so, you may
377 corrupt the data structure (especially when changing it's size).
379 Every operation on a packet is considered to be \e within this packet and \e without and
380 following packet. When inserting or erasing data, the data ranges are all adjusted
381 accordingly. So the following are \e not the same even though \c eth.end(), \c ip.end() and \c
382 udp.end() are identical.
385 eth.data().insert(eth.data().end(), 5, 0x01);
386 assert( eth.data().end() == ip.data().end() + 5
387 && ip.data().end() == udp.data().end() );
389 // Or alternatively: (You could even use eth.data().end() here ... it's the same)
390 ip.data().insert(ip.data().end(), 5, 0x01);
391 assert( eth.data().end() == ip.data().end()
392 && ip.data().end() == udp.data().end() + 5 );
395 \warning When accessing the packet data via the container interface, you may easily build
396 invalid packets since the packet will not be validated against it's protocol.
399 \section packet_usage_fields Protocol fields
401 When working with concrete protocols, the packet library provides direct access to all the
402 protocol information.
405 udp->source() = 2000u;
406 udp->destination() = 2001u;
408 ip->source() = senf::INet4Address::from_string("192.168.0.1");
409 ip->destination() = senf::INet4Address::from_string("192.168.0.2");
410 eth->source() = senf::MACAddress::from_string("00:11:22:33:44:55");
411 eth->destination() = senf::MACAddress::from_string("00:11:22:33:44:66");
414 The protocol field members above do \e not return references, they return parser instances.
415 Protocol fields are accessed via parsers. A parser is a very lightweight class which points into
416 the raw packet data and converts between raw data bytes and it's interpreted value: For example
417 a senf::UInt16Parser accesses 2 bytes (in network byte order) and converts them to or from a 16
418 bit integer. There are a few properties about parsers which need to be understood:
420 \li Parsers are created only temporarily when needed. They are created when accessing a protocol
421 field and are returned by value.
423 \li A parser never contains a value itself, it just references a packets data container.
425 \li Parsers can be built using other parsers and may have members which return further parsers.
427 The top-level interface to a packets protocol fields is provided by a protocol parser. This
428 protocol parser is a composite parser which has members to access the protocol fields (compare
429 with the example code above). Some protocol fields may be more complex than a simple value. In
430 this case, those accessors may return other composite parsers or collection parsers. Ultimately,
431 a value parser will be returned.
433 The simple value parsers which return plain values (integer numbers, network addresses etc) can
434 be used like those values and can also be assigned corresponding values. More complex parsers
435 don't allow simple assignment. However, they can always be copied from another parser <em>of the
436 same type</em> using the generalized parser assignment. This type of assignment also works for
437 simple parsers and is then identical to a normal assignment.
440 // Copy the complete udp parser from udp packet 2 to packet 1
441 udp1.parser() << udp2.parser();
444 Additionally, the parsers have a parser specific API which allows to manipulate or query the
447 This is a very abstract description of the parser structure. For a more concrete description, we
448 need to differentiate between the different parser types
450 \subsection packet_usage_fields_value Value parsers
452 We have already seen value parsers: These are the lowest level building blocks witch parse
453 numbers, addresses etc. They return some type of value and can be assigned such a value. More
454 formally, they have a \c value_type typedef member which gives the type of value they accept and
455 they have an overloaded \c value() member which is used to read or set the value. Some parsers
456 have additional functionality: The numeric parser for Example provide conversion and arithmetic
457 operators so they can be used like a numeric value.
459 If you have a value parser \c valueParser with type \c ValueParser, the following will always be
462 // You can read the value and assign it to a variable of the corresponding value_type
463 ValueParser::value_type v (valueParser.value());
465 // You can assign that value to the parser
466 valueParser.value(v);
468 // The assignment can also be done using the generic parser assignment
473 \subsection packet_usage_fields_composite Composite and protocol parsers
475 A composite parser is a parser which just combines several other parsers into a structure: For
476 example, the senf::EthernetPacketParser has members \c destination(), \c source() and \c
477 type_length(). Those members return parsers again (in this case value parsers) to access the
480 Composite parsers can be nested; A composite parser may be returned by another composite
481 parser. The protocol parser is a composite parser which defines the field for a specific
482 protocol header like Ethernet.
484 \subsection packet_usage_fields_collection Collection parsers
486 Besides simple composites, the packet library has support for more complex collections.
488 \li The senf::ArrayParser allows to repeat an arbitrary parser a fixed number of times.
489 \li senf::VectorParser and senf::ListParser are two different types of lists with variable
491 \li The senf::VariantParser is a discriminated union: It will select one of several parsers
492 depending on the value of a discriminant.
495 \subsubsection packet_usage_collection_vector Vector and List Parsers
497 Remember, that a parser does \e not contain any data: It only points into the raw data
498 container. This is also true for the collection parsers. VectorParser and ListParser provide an
499 interface which looks like an STL container to access the elements.
501 We will use an MLDv2 Query as an example (see <a
502 href="http://tools.ietf.org/html/rfc3810#section-5">RFC 3810</a>).
505 MLDv2QueryPacket mld = ...;
507 // Instantiate a collection wrapper for the source list
508 MLDv2QueryPacket::Parser::sources_t::container sources (mld->sources());
510 // Iterate over all the addresses in that list
511 for (MLDv2QueryPacket::Parser::sources_t::container::iterator i (sources.begin());
512 i != sources.end(); ++i)
513 std::cout << *i << std::endl;
516 Beside other fields, the MLDv2Query consists of a list of source addresses. The \c sources()
517 member returns a VectorParser for these addresses. The collection parsers can only be accessed
518 completely using a container wrapper. This is, what we do in above example.
520 The wrapper can also be used to manipulate that list. Here we copy a list of addresses from an
521 \c std::vector into the packet:
524 std::vector<senf::INet6Address> addrs (...);
526 sources.resize(addrs.size());
527 std::copy(addrs.begin(), addrs.end(), sources.begin())
530 Collection parsers may also be nested. To access a nested collection parser, such a container
531 wrapper should be allocated for each level. An MLD Report (which is a composite parser) includes
532 a list of multicast address records called \c records(). Each record is again a composite which
533 contains a list of sources called \c sources():
536 MLDv2ReportPacket report = ...;
538 // Instantiate a collection wrapper for the list of records:
539 MLDv2ReportPacket::Parser::records_t::container records (report->records());
541 // Iterate over the multicast address records
542 for (MLDv2ReportPacket::Parser::records_t::container::iterator i (records.begin());
543 i != records.end(); ++i) {
544 // Allocate a collection wrapper for the multicast address record
545 typedef MLDv2ReportPackte::Parser::records_t::value_type::sources_t Sources;
546 Sources::container sources (i->sources());
548 // Iterate over the sources in this record
549 for (Sources::container::iterator i (sources.begin());
550 i != sources.end(); ++i)
551 std::cout << *i << std::endl;
555 In this example we also see how to find the type of a parser or container wrapper.
556 \li Composite parsers have typedefs for each their fields with a \c _t postfix
557 \li The vector or list parsers have a \c value_type typedef which gives the type of the
560 By traversing this hierarchical structure, the types of all the fields can be found.
562 The container wrapper is only temporary (even though it has a longer lifetime than a
563 parser). Any change made to the packet not via the collection wrapper has the potential to
564 invalidate the wrapper if it changes the packets size.
567 senf::VectorParser_Container Interface of the vector parser container wrapper \n
568 senf::ListParser_Container Interface of the list parser container wrapper
571 \subsubsection packet_usage_collection_variant The Variant Parser
573 The senf::VariantParser is a discriminated union of parsers. It is also used for optional fields
574 (using senf::VoidPacketParser as one possible variant which is a parser parsing nothing). A
575 senf::VariantParser is not really a collection in the strict sense: It only ever contains one
576 element, the \e type of which is determined by the discriminant.
578 For Example, we look at the DTCP HELLO Packet as defined in the UDLR Protocol (see <a
579 href="http://tools.ietf.org/html/rfc3077">RFC 3077</a>)
582 DTCPHelloPacket hello (...);
584 if (hello->ipVersion() == 4) {
585 typedef DTCPHelloPacket::Parser::v4fbipList_t FBIPList;
586 FBIPList::container fbips (hello->v4fbipList());
587 for (FBIPList::container::iterator i (fbips.begin()); i != fbips.end(); ++i)
588 std::cout << *i << std::endl;
590 else { // if (hello->ipVersion() == 6)
591 typedef DTCPHelloPacket::Parser::v6fbipList_t FBIPList;
592 FBIPList::container fbips (hello->v6fbipList());
593 for (FBIPList::container::iterator i (fbips.begin()); i != fbips.end(); ++i)
594 std::cout << *i << std::endl;
598 This packet has a field \c ipVersion() which has a value of 4 or 6. Depending on the version,
599 the packet contains a list of IPv4 or IPv6 addresses. Only one of the fields \c v4fbipList() and
600 \c v6fbipList() is available at a time. Which one is decided by the value of \c
601 ipVersion(). Trying to access the wrong one will provoke undefined behavior.
603 Here we have used the variants discriminant (the \c ipVersion() field) to select, which field to
604 parse. More generically, every variant field should have a corresponding member to test for it's
607 if (hello->has_v4fbipList()) {
610 else { // if (hello->has_v6fbipList())
615 A variant can have more than 2 possible types and you can be sure, that exactly one type will be
616 accessible at any time.
618 It is not possible to change a variant by simply changing the discriminant:
621 hello->ipVersion() = 6;
623 Instead, for each variant field there is a special member which switches the variant to that
624 type. After switching the type, the field will be in it's initialized (that is mostly zero)
627 std::vector<senf::INet6Address> addrs (...);
629 // Initialize the IPv6 list
630 hello->init_v6fbipList();
632 // Copy values into that list
633 DTCPHelloPacket::Parser::v6fbipList_t::container fbips (hello->v6fbipList());
634 fbips.resize(addrs.size());
635 std::copy(addrs.begin(), addrs.end(), fbips.begin());
638 \note Here we have documented the default variant interface as it is preferred. It is possible
639 to define variants in a different way giving other names to the special members (\c has_\e
640 name or \c init_\e name etc.). This must be documented with the composite or protocol parser
641 which defines the variant.
644 /** \page packet_new Defining new Packet types
646 Each packet is specified by the following two components:
648 \li A protocol parser which defines the protocol specific fields
649 \li A packet type class which is a policy class defining the packet
653 \see <a href="../../../HowTos/NewPacket/doc/html/index.html">NewPacket HowTo</a>
655 \section packet_new_parser The protocol parser
657 The protocol parser is simply a composite parser. It defines all the protocol
658 fields. Additionally, the protocol parser may have additional members which will then be
659 accessible via the \c -> operator of the packet. Possibilities here are e.g. checksum
660 calculation and validation, packet validation as a whole and so on.
662 Defining a protocol parser is quite simple:
664 struct EthernetPacketParser : public PacketParserBase
666 # include SENF_FIXED_PARSER()
668 SENF_PARSER_FIELD( destination, MACAddressParser );
669 SENF_PARSER_FIELD( source, MACAddressParser );
670 SENF_PARSER_FIELD( type_length, UInt16Parser );
672 SENF_PARSER_FINALIZE(EthernetPacketParser);
676 There are a lot of other possibilities to define fields. See \ref packetparsermacros for a
677 detailed description of the macro language which is used to define composite parsers.
680 \ref packetparsermacros
682 \section packet_new_type The packet type policy class
684 This is a class which provides all the information needed to integrate the new packet type into
687 \li It provides the type of the protocol parser to use
688 \li It provides information on how the next protocol can be found and where the payload resides
690 \li It provides methods to initialize a new packet and get information about the packet size
692 All this information is provided via static or typedef members.
695 struct EthernetPacketType
696 : public PacketTypeBase,
697 public PacketTypeMixin<EthernetPacketType, EtherTypes>
699 typedef PacketTypeMixin<EthernetPacketType, EtherTypes> mixin;
700 typedef ConcretePacket<EthernetPacketType> packet;
701 typedef EthernetPacketParser parser;
703 using mixin::nextPacketRange;
704 using mixin::initSize;
707 static factory_t nextPacketType(packet p);
708 static void dump(packet p, std::ostream & os);
709 static void finalize(packet p);
712 typedef EthernetPacketType::packet EthernetPacket;
715 The definition of senf::EthernetPacket is quite straight forward. This template works for most
718 \see \ref senf::PacletTypeMixin
719 \ref senf::PacketTypeBase
720 \ref senf::PacketRegistry
727 // c-file-style: "senf"
728 // indent-tabs-mode: nil
729 // ispell-local-dictionary: "american"
731 // compile-command: "scons -u doc"