[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[minion-cvs] Support functionality for directory server generation.
Update of /home/minion/cvsroot/src/minion/lib/mixminion
In directory moria.mit.edu:/tmp/cvs-serv20744/lib/mixminion
Modified Files:
Common.py Crypto.py ServerInfo.py benchmark.py test.py
Log Message:
Support functionality for directory server generation.
Common:
- Add method to format time for use in a filename.
- Add an IntervalSet class to keep track of server liveness
Crypto:
- Add pk_same_public_key so we can tell whether identity keys
are the same.
ServerInfo:
- Keep track of whether we've validated the desc or not.
- Make digest-and-sign logic generic.
test:
- Add tests for IntervalSet
Index: Common.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Common.py,v
retrieving revision 1.37
retrieving revision 1.38
diff -u -d -r1.37 -r1.38
--- Common.py 29 Dec 2002 20:25:32 -0000 1.37
+++ Common.py 31 Dec 2002 04:48:46 -0000 1.38
@@ -5,14 +5,15 @@
Common functionality and utility code for Mixminion"""
-__all__ = [ 'LOG', 'LogStream', 'MixError', 'MixFatalError',
+__all__ = [ 'IntervalSet', 'LOG', 'LogStream', 'MixError', 'MixFatalError',
'MixProtocolError', 'ceilDiv', 'checkPrivateDir',
'createPrivateDir', 'floorDiv', 'formatBase64', 'formatDate',
- 'formatTime', 'installSignalHandlers', 'isSMTPMailbox',
- 'onReset', 'onTerminate', 'previousMidnight', 'secureDelete',
- 'stringContains', 'waitForChildren' ]
+ 'formatFnameTime', 'formatTime', 'installSignalHandlers',
+ 'isSMTPMailbox', 'onReset', 'onTerminate', 'previousMidnight',
+ 'secureDelete', 'stringContains', 'waitForChildren' ]
import base64
+import bisect
import calendar
import os
import re
@@ -536,6 +537,158 @@
gmt = time.gmtime(when+1) # Add 1 to make sure we round down.
return "%04d/%02d/%02d" % (gmt[0],gmt[1],gmt[2])
+def formatFnameTime(when=None):
+ """Given a time in seconds since the epoch, returns a date value suitable
+ for use as part of a fileame. Defaults to the current time."""
+ # XXXX002 test
+ if when is None:
+ when = time.time()
+ return time.strftime("%Y%m%d%H%M%S", time.localtime(when))
+
+#----------------------------------------------------------------------
+# InteralSet
+
+class IntervalSet:
+ """An IntervalSet is a mutable set of numeric intervals, closed below and
+ open above. Not very optimized for now. Supports "+" for union, "-"
+ for disjunction, and "*" for intersection."""
+ ## Fields:
+ # edges: an ordered list of boundary points between interior and
+ # exterior points, of the form (x, '+') for an 'entry' and
+ # (x, '-') for an 'exit' boundary.
+ #
+ # FFFF There must be a more efficient algorithm for this, but we're so
+ # FFFF far from the critical path here that I'm not going to look for it
+ # FFFF for quite a while.
+ def __init__(self, intervals=None):
+ """Given a list of (start,end) tuples, construct a new IntervalSet.
+ Tuples are ignored if start>=end."""
+ self.edges = []
+ if intervals:
+ for start, end in intervals:
+ if start < end:
+ self.edges.append((start, '+'))
+ self.edges.append((end, '-'))
+ def copy(self):
+ """Create a new IntervalSet with the same intervals as this one."""
+ r = IntervalSet()
+ r.edges = self.edges[:]
+ return r
+ def __iadd__(self, other):
+ """self += b : Causes this set to contain all points in itself but not
+ in b."""
+ self.edges += other.edges
+ self._cleanEdges()
+ return self
+ def __isub__(self, other):
+ """self -= b : Causes this set to contain all points in itself but not
+ in b"""
+ for t, e in other.edges:
+ if e == '+':
+ self.edges.append((t, '-'))
+ else:
+ self.edges.append((t, '+'))
+ self._cleanEdges()
+ return self
+ def __imul__(self, other):
+ """self *= b : Causes this set to contain all points in both itself and
+ b."""
+ self.edges += other.edges
+ self._cleanEdges(2)
+ return self
+
+ def _cleanEdges(self, nMin=1):
+ """Internal helper method: to be called when 'edges' is in a dirty
+ state, containing entry and exit points that don't create a
+ well-defined set of intervals. Only those points that are 'entered'
+ nMin times or more are retained.
+ """
+ edges = self.edges
+ edges.sort()
+ depth = 0
+ newEdges = [ ('X', 'X') ] #marker value; will be removed.
+ for t, e in edges:
+ # Traverse the edges in order; keep track of how many more
+ # +'s we have seen than -'s. Whenever that number increases
+ # above nMin, add a +. Whenever that number drops below nMin,
+ # add a - ... but if the new edge would cancel out the most
+ # recently added one, then delete the most recently added one.
+ if e == '+':
+ depth += 1
+ if depth == nMin:
+ if newEdges[-1] == (t, '-'):
+ del newEdges[-1]
+ else:
+ newEdges.append((t, '+'))
+ if e == '-':
+ if depth == nMin:
+ if newEdges[-1] == (t, '+'):
+ del newEdges[-1]
+ else:
+ newEdges.append((t, '-'))
+ depth -= 1
+ assert depth == 0
+ del newEdges[0]
+ self.edges = newEdges
+
+ def __add__(self, other):
+ r = self.copy()
+ r += other
+ return r
+
+ def __sub__(self, other):
+ r = self.copy()
+ r -= other
+ return r
+
+ def __mul__(self, other):
+ r = self.copy()
+ r *= other
+ return r
+
+ def __contains__(self, other):
+ """'a in self' is true when 'a' is a number contained in some interval
+ in this set, or when 'a' is an IntervalSet that is a subset of
+ this set."""
+ if isinstance(other, IntervalSet):
+ return self*other == other
+ idx = bisect.bisect_right(self.edges, (other, '-'))
+ return idx < len(self.edges) and self.edges[idx][1] == '-'
+
+ def isEmpty(self):
+ """Return true iff this set contains no points"""
+ return len(self.edges) == 0
+
+ def __nonzero__(self):
+ """Return true iff this set contains some points"""
+ return len(self.edges) != 0
+
+ def __repr__(self):
+ s = [ "(%s,%s)"%(start,end) for start, end in self.getIntervals() ]
+ return "IntervalSet([%s])"%",".join(s)
+
+ def getIntervals(self):
+ """Returns a list of (start,end) tuples for a the intervals in this
+ set."""
+ s = []
+ for i in range(0, len(self.edges), 2):
+ s.append((self.edges[i][0], self.edges[i+1][0]))
+ return s
+
+ def _checkRep(self):
+ """Helper function: raises AssertionError if this set's data is
+ corrupted."""
+ assert (len(self.edges) % 2) == 0
+ for i in range(0, len(self.edges), 2):
+ assert self.edges[i][0] < self.edges[i+1][0]
+ assert self.edges[i][1] == '+'
+ assert self.edges[i+1][1] == '-'
+ assert i == 0 or self.edges[i-1][0] < self.edges[i][0]
+
+ def __cmp__(self, other):
+ """A == B iff A and B contain exactly the same intervals."""
+ return cmp(self.edges, other.edges)
+
#----------------------------------------------------------------------
# SMTP address functionality
Index: Crypto.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/Crypto.py,v
retrieving revision 1.30
retrieving revision 1.31
diff -u -d -r1.30 -r1.31
--- Crypto.py 29 Dec 2002 20:28:01 -0000 1.30
+++ Crypto.py 31 Dec 2002 04:48:46 -0000 1.31
@@ -256,6 +256,11 @@
"""Reads an ASN1 representation of a public key from external storage."""
return _ml.rsa_decode_key(s,1)
+def pk_same_public_key(key1, key2):
+ """Return true iff key1 and key2 are the same key."""
+ #XXXX TEST
+ return key1.encode_key(1) == key2.encode_key(1)
+
def pk_PEM_save(rsa, filename, password=None):
"""Save a PEM-encoded private key to a file. If <password> is provided,
encrypt the key using the password."""
Index: ServerInfo.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/ServerInfo.py,v
retrieving revision 1.27
retrieving revision 1.28
diff -u -d -r1.27 -r1.28
--- ServerInfo.py 29 Dec 2002 20:46:54 -0000 1.27
+++ ServerInfo.py 31 Dec 2002 04:48:46 -0000 1.28
@@ -11,6 +11,7 @@
__all__ = [ 'ServerInfo' ]
import re
+import time
import mixminion.Config
import mixminion.Crypto
@@ -37,6 +38,8 @@
# tmp alias to make this easier to spell.
C = mixminion.Config
class ServerInfo(mixminion.Config._ConfigFile):
+ ## Fields
+ # isValidated: DOCDOC
"""A ServerInfo object holds a parsed server descriptor."""
_restrictFormat = 1
_syntax = {
@@ -77,6 +80,7 @@
}
def __init__(self, fname=None, string=None, assumeValid=0):
+ self._isValidated = 0
mixminion.Config._ConfigFile.__init__(self, fname, string, assumeValid)
LOG.trace("Reading server descriptor %s from %s",
self['Server']['Nickname'],
@@ -95,6 +99,8 @@
identityBytes = identityKey.get_modulus_bytes()
if not (MIN_IDENTITY_BYTES <= identityBytes <= MAX_IDENTITY_BYTES):
raise ConfigError("Invalid length on identity key")
+ if server['Published'] > time.time() + 600:
+ raise ConfigError("Server published in the future")
if server['Valid-Until'] <= server['Valid-After']:
raise ConfigError("Server is never valid")
if server['Contact'] and len(server['Contact']) > MAX_CONTACT:
@@ -136,6 +142,8 @@
# FFFF When a better client module system exists, check the
# FFFF module descriptors.
+ self._isValidated = 1
+
def getNickname(self):
"""Returns this server's nickname"""
return self['Server']['Nickname']
@@ -161,6 +169,10 @@
to this server."""
return IPV4Info(self.getAddr(), self.getPort(), self.getKeyID())
+ def isValidated(self):
+ "DOCDOC"
+ return self._isValidated
+
#----------------------------------------------------------------------
def getServerInfoDigest(info):
"""Calculate the digest of a server descriptor"""
@@ -172,38 +184,55 @@
no values."""
return _getServerInfoDigestImpl(info, rsa)
+_leading_whitespace_re = re.compile(r'^[ \t]+', re.M)
_trailing_whitespace_re = re.compile(r'[ \t]+$', re.M)
-_special_line_re = re.compile(r'^(?:Digest|Signature):.*$', re.M)
-def _getServerInfoDigestImpl(info, rsa=None):
+_abnormal_line_ending_re = re.compile(r'\r\n?')
+def _cleanForDigest(s):
+ "DOCDOC"
+ # should be shared with config, serverinfo.
+ s = _abnormal_line_ending_re.sub("\n", s)
+ s = _trailing_whitespace_re.sub("", s)
+ s = _leading_whitespace_re.sub("", s)
+ if s[-1] != "\n":
+ s += "\n"
+ return s
+
+def _getDigestImpl(info, regex, digestField=None, sigField=None, rsa=None):
"""Helper method. Calculates the correct digest of a server descriptor
(as provided in a string). If rsa is provided, signs the digest and
- creates a new descriptor. Otherwise just returns the digest."""
+ creates a new descriptor. Otherwise just returns the digest.
- # The algorithm's pretty easy. We just find the Digest and Signature
- # lines, replace each with an 'Empty' version, and calculate the digest.
- info = _trailing_whitespace_re.sub("", info)
- if not info.startswith("[Server]"):
- raise ConfigError("Must begin with server section")
- def replaceFn(s):
- if s.group(0)[0] == 'D':
- return "Digest:"
- else:
- return "Signature:"
- info = _special_line_re.sub(replaceFn, info)
+ DOCDOC: NO LONGER QUITE TRUE
+ """
+ info = _cleanForDigest(info)
+ def replaceFn(m):
+ s = m.group(0)
+ return s[:s.index(':')+1]
+ info = regex.sub(replaceFn, info, 2)
digest = mixminion.Crypto.sha1(info)
if rsa is None:
return digest
- # If we got an RSA key, we need to add the digest and signature.
-
+
signature = mixminion.Crypto.pk_sign(digest,rsa)
digest = formatBase64(digest)
signature = formatBase64(signature)
- def replaceFn2(s, digest=digest, signature=signature):
- if s.group(0)[0] == 'D':
- return "Digest: "+digest
+ def replaceFn2(s, digest=digest, signature=signature,
+ digestField=digestField, sigField=sigField):
+ if s.group(0).startswith(digestField):
+ return "%s: %s" % (digestField, digest)
else:
- return "Signature: "+signature
+ assert s.group(0).startswith(sigField)
+ return "%s: %s" % (sigField, signature)
- info = _special_line_re.sub(replaceFn2, info)
+ info = regex.sub(replaceFn2, info, 2)
return info
+
+_special_line_re = re.compile(r'^(?:Digest|Signature):.*$', re.M)
+def _getServerInfoDigestImpl(info, rsa=None):
+ return _getDigestImpl(info, _special_line_re, "Digest", "Signature", rsa)
+
+_dir_special_line_re = re.compile(r'^Directory(?:Digest|Signature):.*$', re.M)
+def _getDirectoryDigestImpl(directory, rsa=None):
+ return _getDigestImpl(directory, _dir_special_line_re,
+ "DirectoryDigest", "DirectorySignature", rsa)
Index: benchmark.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/benchmark.py,v
retrieving revision 1.20
retrieving revision 1.21
diff -u -d -r1.20 -r1.21
--- benchmark.py 29 Dec 2002 20:46:54 -0000 1.20
+++ benchmark.py 31 Dec 2002 04:48:47 -0000 1.21
@@ -359,10 +359,7 @@
#----------------------------------------------------------------------
def directoryTiming():
print "#========== DESCRIPTORS AND DIRECTORIES =============="
- from mixminion.server.ServerKeys import ServerKeyring, \
- generateServerDescriptorAndKeys
- gen = generateServerDescriptorAndKeys
- homedir = mix_mktemp()
+ from mixminion.server.ServerKeys import ServerKeyring
confStr = """
[Server]
EncryptIdentityKey: no
@@ -648,8 +645,6 @@
_ml.rsa_make_public_key(n,e)
#----------------------------------------------------------------------
-import base64
-import binascii
def timeAll(name, args):
cryptoTiming()
Index: test.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/test.py,v
retrieving revision 1.51
retrieving revision 1.52
diff -u -d -r1.51 -r1.52
--- test.py 29 Dec 2002 20:34:36 -0000 1.51
+++ test.py 31 Dec 2002 04:48:47 -0000 1.52
@@ -51,6 +51,7 @@
import mixminion.server.ServerConfig
import mixminion.server.ServerKeys
import mixminion.server.ServerMain
+import mixminion.directory.ServerList
from mixminion.Common import *
from mixminion.Common import Log, _FileLogHandler, _ConsoleLogHandler
from mixminion.Config import _ConfigFile, ConfigError, _parseInt
@@ -214,6 +215,192 @@
"foo@bar;cat /etc/shadow;echo ","foo bar@baz.com",
"a@b@c"):
self.assert_(not isSMTPMailbox(addr))
+
+ def test_intervalset(self):
+ eq = self.assertEquals
+ nil = IntervalSet()
+ nil2 = IntervalSet()
+ nil._checkRep()
+ self.assert_(nil.isEmpty())
+ self.assert_(nil == nil2)
+ eq(repr(nil), "IntervalSet([])")
+ eq([], nil.getIntervals())
+ nil3 = IntervalSet([(10, 0)])
+ eq([], nil3.getIntervals())
+
+ oneToTen = IntervalSet([(1,10)])
+ fourToFive = IntervalSet([(4,5)])
+ zeroToTen = IntervalSet([(0,10)])
+ zeroToTwenty = IntervalSet([(0,20)])
+ tenToTwenty = IntervalSet([(10,20)])
+ oneToTwenty = IntervalSet([(1,20)])
+ fifteenToFifty = IntervalSet([(15,50)])
+
+ eq(zeroToTen.getIntervals(), [(0, 10)])
+ for iset in oneToTen, fourToFive, zeroToTen, zeroToTwenty, oneToTwenty:
+ iset._checkRep()
+
+ checkEq = self._intervalEq
+
+ # Tests for addition: A + B, where...
+ # 1. A and B are empty.
+ checkEq(nil+nil, nil, [])
+ # 2. Just A or B is empty.
+ checkEq(nil+oneToTen, oneToTen+nil, oneToTen, [(1,10)])
+ # 3. A contains B, or vice versa.
+ checkEq(oneToTen+fourToFive, fourToFive+oneToTen, oneToTen)
+ checkEq(oneToTen+zeroToTwenty, zeroToTwenty)
+ # 4. A == B
+ checkEq(oneToTen+oneToTen, oneToTen)
+ # 5. A and B are disjoint and don't touch.
+ checkEq(oneToTen+fifteenToFifty, fifteenToFifty+oneToTen,
+ [(1,10),(15,50)])
+ # 6. A and B are disjoint and touch
+ checkEq(oneToTen+tenToTwenty, tenToTwenty+oneToTen, oneToTwenty)
+ # 7. A and B overlap on one side only.
+ checkEq(oneToTwenty+fifteenToFifty,
+ fifteenToFifty+oneToTwenty,
+ "IntervalSet([(1,50)])")
+ # 8. A nice complex situation.
+ fromPrimeToPrime = IntervalSet([(2,3),(5,7),(11,13),(17,19),(23,29)])
+ fromSquareToSquare = IntervalSet([(1,4),(9,16),(25,36)])
+ fromFibToFib = IntervalSet([(1,1),(2,3),(5,8),(13,21),(34,55)])
+ x = fromPrimeToPrime.copy()
+ x += fromSquareToSquare
+ x += fromSquareToSquare
+ checkEq(fromPrimeToPrime+fromSquareToSquare,
+ fromSquareToSquare+fromPrimeToPrime,
+ x,
+ [(1,4),(5,7),(9,16),(17,19),(23,36)])
+ checkEq(fromSquareToSquare+fromFibToFib,
+ [(1,4),(5,8),(9,21),(25,55)])
+ checkEq(fromPrimeToPrime+fromFibToFib,
+ [(2,3),(5,8),(11,21),(23,29),(34,55)])
+
+ # Now, subtraction!
+ # 1. Involving nil.
+ checkEq(nil-nil, nil, [])
+ checkEq(fromSquareToSquare-nil, fromSquareToSquare)
+ checkEq(nil-fromSquareToSquare, nil)
+ # 2. Disjoint ranges.
+ checkEq(fourToFive-tenToTwenty, fourToFive)
+ checkEq(tenToTwenty-fourToFive, tenToTwenty)
+ # 3. Matching on one side
+ checkEq(oneToTwenty-oneToTen, tenToTwenty)
+ checkEq(oneToTwenty-tenToTwenty, oneToTen)
+ checkEq(oneToTen-oneToTwenty, nil)
+ checkEq(tenToTwenty-oneToTwenty, nil)
+ # 4. Overlapping on one side
+ checkEq(fifteenToFifty-oneToTwenty, [(20,50)])
+ checkEq(oneToTwenty-fifteenToFifty, [(1,15)])
+ # 5. Overlapping in the middle
+ checkEq(oneToTen-fourToFive, [(1,4),(5,10)])
+ checkEq(fourToFive-oneToTen, nil)
+ # 6. Complicated
+ checkEq(fromPrimeToPrime-fromSquareToSquare,
+ [(5,7),(17,19),(23,25)])
+ checkEq(fromSquareToSquare-fromPrimeToPrime,
+ [(1,2),(3,4),(9,11),(13,16),(29,36)])
+ checkEq(fromSquareToSquare-fromFibToFib,
+ [(1,2),(3,4),(9,13),(25,34)])
+ checkEq(fromFibToFib-fromSquareToSquare,
+ [(5,8),(16,21),(36,55)])
+ # 7. Identities
+ for a in (fromPrimeToPrime, fromSquareToSquare, fromFibToFib, nil):
+ for b in (fromPrimeToPrime, fromSquareToSquare, fromFibToFib, nil):
+ checkEq(a-b+b, a+b)
+ checkEq(a+b-b, a-b)
+
+ ## Test intersection
+ # 1. With nil
+ checkEq(nil*nil, nil*fromFibToFib, oneToTen*nil, nil, [])
+ # 2. Self
+ for iset in oneToTen, fromSquareToSquare, fourToFive:
+ checkEq(iset, iset*iset)
+ # 3. A disjoint from B
+ checkEq(oneToTen*fifteenToFifty, fifteenToFifty*oneToTen, nil)
+ # 4. A disjoint from B but touching.
+ checkEq(oneToTen*tenToTwenty, tenToTwenty*oneToTen, nil)
+ # 5. A contains B at the middle.
+ checkEq(oneToTen*fourToFive, fourToFive*oneToTen, fourToFive)
+ # 6. A contains B at one end
+ checkEq(oneToTen*oneToTwenty, oneToTwenty*oneToTen, oneToTen)
+ checkEq(tenToTwenty*oneToTwenty, oneToTwenty*tenToTwenty, tenToTwenty)
+ # 7. A and B overlap without containment.
+ checkEq(fifteenToFifty*oneToTwenty, oneToTwenty*fifteenToFifty,
+ [(15,20)])
+ # 8. Tricky cases
+ checkEq(fromPrimeToPrime*fromSquareToSquare,
+ fromSquareToSquare*fromPrimeToPrime,
+ [(2,3),(11,13),(25,29)])
+ checkEq(fromPrimeToPrime*fromFibToFib,
+ fromFibToFib*fromPrimeToPrime,
+ [(2,3),(5,7),(17,19)])
+ checkEq(fromSquareToSquare*fromFibToFib,
+ fromFibToFib*fromSquareToSquare,
+ [(2,3),(13,16),(34,36)])
+ # 9. Identities
+ for a in (fromPrimeToPrime, fromSquareToSquare, fromFibToFib, oneToTen,
+ fifteenToFifty, nil):
+ self.assert_((not a) == a.isEmpty() == (a == nil))
+ for b in (fromPrimeToPrime, fromSquareToSquare, fromFibToFib,
+ oneToTen, fifteenToFifty, nil):
+ checkEq(a*b,b*a)
+ checkEq(a-b, a*(a-b), (a-b)*a)
+ checkEq(b*(a-b), (a-b)*b, nil)
+ checkEq(a-b, a-a*b)
+ checkEq((a-b)+a*b, a)
+ checkEq((a-b)*(b-a), nil)
+ checkEq((a-b)+(b-a)+a*b, a+b)
+
+ ## Contains
+ t = self.assert_
+ # 1. With nil
+ t(5 not in nil)
+ t(oneToTen not in nil)
+ t(fromFibToFib not in nil)
+ # 2. Self in self
+ for iset in nil, oneToTen, tenToTwenty, fromSquareToSquare:
+ t(iset in iset)
+ # 3. Simple sets: closed below, open above.
+ t(1 in oneToTen)
+ t(2 in oneToTen)
+ t(9.9 in oneToTen)
+ t(10 not in oneToTen)
+ t(0 not in oneToTen)
+ t(11 not in oneToTen)
+ # 4. Simple sets: A contains B.
+ t(fourToFive in oneToTen) # contained wholly
+ t(oneToTen in zeroToTen) #contains on one side.
+ t(oneToTwenty not in oneToTen) #disjoint on one side
+ t(oneToTen not in tenToTwenty) #disjoint but touching
+ t(fourToFive not in tenToTwenty) #disjoint, not touching
+ # 5. Complex sets: closed below, open above
+ t(0 not in fromSquareToSquare)
+ t(1 in fromSquareToSquare)
+ t(2 in fromSquareToSquare)
+ t(4 not in fromSquareToSquare)
+ t(8 not in fromSquareToSquare)
+ t(9 in fromSquareToSquare)
+ t(15 in fromSquareToSquare)
+ t(16 not in fromSquareToSquare)
+ t(35 in fromSquareToSquare)
+ t(36 not in fromSquareToSquare)
+ t(100 not in fromSquareToSquare)
+
+ def _intervalEq(self, a, *others):
+ eq = self.assertEquals
+ for b in others:
+ if isinstance(b, IntervalSet):
+ eq(a,b)
+ b._checkRep()
+ elif isinstance(b, types.StringType):
+ eq(repr(a), b)
+ elif isinstance(b, types.ListType):
+ eq(a.getIntervals(), b)
+ else:
+ raise MixError()
+ a._checkRep()
#----------------------------------------------------------------------