# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""XML schemas for FuelPacks.

A FuelPack is a single XML file.

The XML file can be edited manually or with the Fuel Pack Editor
(fpe.py).  It should be maintained in Perforce so it can be fully
revision-controlled.

See EEP 12 for the XML schema (DTD-by-example).

"""

import os
import re
import sys
import tokenize

from sigma.util.xmlparse import ElementClass, ParseError
from sigma.util.xmlparse import Integer, String, Boolean, Timestamp
from sigma.util import ostypes, paramtypes, uuid
from sigma.sched.tasks import Task

# Exceptions raised by newsubpolicy()
class Error(Exception): pass
class CycleError(Error): pass
class DuplicateError(Error): pass
class MissingError(Error): pass
class ReferentialIntegrityError(Error): pass

# Special policy ids
ROOT_POLICY_ID = 1
TRASH_POLICY_ID = 2
SPECIAL_POLICY_IDS = (ROOT_POLICY_ID, TRASH_POLICY_ID)

# Data types

def IdRef(arg):
    """An IdRef is either an int or two ints separated by a colon.

    The Python representation is either an int or a tuple of two ints.

    The ints may be longs, of course.

    """
    i = arg.find(":")
    if i < 0:
        return Integer(arg)
    hi, lo = arg[:i], arg[i+1:]
    return Integer(hi), Integer(lo)

def IdRef_render(x):
    if isinstance(x, int):
        return str(x)
    if isinstance(x, tuple) and len(x) == 2:
        hi, lo = x
        if isinstance(hi, int) and isinstance(lo, int):
            return str(hi) + ":" + str(lo)
    raise ParseError("Can't unparse IdRef %s" % repr(x))

IdRef.__render__ = IdRef_render

# Elements

class Description(ElementClass):
    __element__ = "description"
    __attributes__ = {}
    __characters__ = "text"

class Documentation(Description):
    __element__ = "documentation"

class Notes(Description):
    __element__ = "notes"

class Value(ElementClass):
    __element__ = "value"
    __attributes__ = {}
    __characters__ = "text"

    def check(self, type):
        assert self.text is not None
        try:
            paramtypes.convert(self.text, type)
        except paramtypes.ParamError as err:
            raise AssertionError(str(err))

class Default(Value):
    __element__ = "default"

class Parameter(ElementClass):
    __element__ = "parameter"
    __attributes__ = {"position": Integer,
                      "name": String,
                      "type": String}
    __children__ = {Description: "description",
                    Default: "default"}

    def check(self):
        assert self.position is not None
        # XXX Only allow 10 parameter positions
        assert self.position >= 1 and self.position <= 10, self.position
        assert self.name is not None
        assert re.match(r"\A[a-zA-Z_]\w*\Z", self.name), self.name
        assert self.type is not None
        assert paramtypes.isvalidtype(self.type), repr(self.type)
        if self.default is not None:
            self.default.check(self.type)

class Implementation(ElementClass):
    __element__ = "implementation"
    __attributes__ = {"ostypes": String,
                      "module": String,
                      "version": String,
                      "procedure": String}

    def check(self):
        assert self.ostypes is not None
        osts = self.ostypes.split(",")
        for ost in osts:
            assert ostypes.isvalid(ost), "Invalid ostype %r" % ost
        # XXX Should we validate the module and procedure?  Punt for now.

class Template(ElementClass):
    __element__ = "template"
    __attributes__ = {"name": String,
                      "id": Integer,
                      "enforceable": Boolean,
                      "category": String,
                      "enterprise": Boolean}
    __children__ = {Description: "description",
                    Documentation: "documentation",
                    Notes: "notes",
                    Parameter: "parameters[]",
                    Implementation: "implementations[]"}

    parents = None # default value for instance attribute, see addparentage()

    def getancestors(self):
        "Returns the set of ancestral policy ids, excluding self.id."
        assert self.parents is not None # Must be using parentage
        anc = set()
        for parent in self.parents:
            if parent.id not in anc:
                anc |= parent.getancestors()
        return anc

    def check(self, catmap):
        assert self.description is not None
        pd = {}
        pn = {}
        for p in self.parameters:
            p.check()
            assert p.position not in pd, p.position # duplicate position
            assert p.name not in pn, p.name # duplicate name
            pd[p.position] = p
            pn[p.name] = p
        posns = sorted(pd.keys())
        assert posns == list(range(1, len(pd)+1)), posns # missing positions
        od = {}
        for i in self.implementations:
            i.check()
            for ost in i.ostypes.split(","):
                assert ost not in od, (ost, self.id) # duplicate ostype
                od[ost] = i
        self.checkcategory(catmap)

    def checkcategory(self, catmap):
        cat = self.category
        assert cat, "template %s: no category" % self.id
        assert cat in catmap, "template %s: bad category %s" % (self.id, cat)

class Templates(ElementClass):
    __element__ = "templates"
    __attributes__ = {}
    __children__ = {Template: "templates[]"}

    def next_template_id(self):
        return uuid.new()

    def get(self, template_id):
        for t in self.templates:
            if t.id == template_id:
                return t
        return None

    def gettemplatebyname(self, name):
        name = name.lower()
        for p in self.templates:
            if p.name.lower() == name:
                return p
        return None

    def check(self, catmap):
        idmap = {}
        namemap = {}
        namedups = {}
        for t in self.templates:
            if t.name not in namemap:
                namemap[t.name] = [t.id]
            else:
                L = namemap[t.name]
                L.append(t.id)
                namedups[t.name] = L
            t.check(catmap)
            assert t.id not in idmap, t.id
            idmap[t.id] = t
        assert not namedups, _formatnamedups(namedups, "template")

def _formatnamedups(namedups, what):
    # Helper for Policies.check()
    parts = []
    for name, L in namedups.items():
        parts.append("%s %s" % (name, tuple(L)))
    return "Duplicate %s names:\n\t" % what + "\n\t".join(parts)

class Argument(ElementClass):
    __element__ = "argument"
    __attributes__ = {"name": String}
    __children__ = {Value: "value"}

    def check(self, template):
        for p in template.parameters:
            if p.name == self.name:
                break
        else:
            assert 0, "No parameter named %r" % self.name
        if self.value is not None:
            self.value.check(p.type)

class Rule(ElementClass):
    __element__ = "rule"
    __attributes__ = {"id": Integer,
                      "template-id": IdRef}
    __children__ = {Argument: "arguments[]"}
    # XXX Do we need to support preset rule deployments too?

    def check(self, templates):
        assert self.id is not None, self
        assert self.id >= 1, self.id
        tid = self.template_id
        assert tid is not None
        if isinstance(tid, tuple):
            fpid, tid = tid
            assert isinstance(fpid, int)
            assert isinstance(tid, int)
            t = None
        else:
            t = templates.get(self.template_id)
            assert t is not None, self.template_id
        ad = {}
        for a in self.arguments:
            if t is not None:
                a.check(t)
            assert a.name not in ad
            ad[a.name] = a

    def __render__(self, f=None, level="", indent="  "):
        if self.arguments:
            ElementClass.__render__(self, f, level, indent)
            return
        # Render argument-less rules more compactly
        if f is None:
            f = sys.stdout
        f.write("%s<%s" % (level, self.__element__))
        attrnames = sorted(self.__attributes__.keys())
        for key in attrnames:
            value = getattr(self, self.__fixname__(key), None)
            if value is not None:
                function = self.__attributes__[key]
                value = function.__render__(value)
                f.write(' %s="%s"' % (key, value))
        f.write(" />\n")

class Subpolicy(ElementClass):
    __element__ = "subpolicy"
    __attributes__ = {"policy-id": IdRef}

    def check(self, policies):
        pid = self.policy_id
        assert pid is not None
        if isinstance(pid, tuple):
            fpid, pid = pid
            assert isinstance(fpid, int)
            assert isinstance(pid, int)
        else:
            assert policies.getpolicy(pid) is not None, pid

class Deployment(ElementClass):
    __element__ = "deployment"
    __attributes__ = {"group": String,
                      "enforce-packet-filter": Boolean,
                      "enforce-host-configuration": Boolean,
                      "alarm": Boolean,
                      "read-only": Boolean}
    __children__ = {Task: "task"}

class Policy(ElementClass):
    __element__ = "policy"
    __attributes__ = {"name": String,
                      "id": Integer}
    __children__ = {Description: "description",
                    Documentation: "documentation",
                    Notes: "notes",
                    Deployment: "deployment", # XXX Is one enough?
                    Subpolicy: "subpolicies[]",
                    Rule: "rules[]"}

    parents = None # default value for instance attribute, see addparentage()

    def getancestors(self):
        "Returns the set of ancestral policy ids, including self.id."
        assert self.parents is not None # Must be using parentage
        anc = set([self.id])
        for parent in self.parents:
            if parent.id not in anc:
                anc |= parent.getancestors()
        return anc

    def check(self, policies, templates):
        assert self.id is not None
        assert self.id >= 1, (self, self.id)
        assert self.description is not None, self.id
        for ss in self.subpolicies:
            ss.check(policies)
        for r in self.rules:
            r.check(templates)

class Policies(ElementClass):
    __element__ = "policies"
    __attributes__ = {}
    __children__ = {Policy: "policies[]"}

    def next_policy_id(self):
        return uuid.new()

    def next_rule_id(self):
        return uuid.new()

    def getpolicybyname(self, name):
        name = name.lower()
        for s in self.policies:
            if s.name.lower() == name:
                return s
        return None

    def getpolicy(self, policy_id):
        for s in self.policies:
            if s.id == policy_id:
                return s
        return None

    def getrule(self, rule_id):
        for s in self.policies:
            for r in s.rules:
                if r.id == rule_id:
                    return r
        return None

    def getclosure(self, policy):
        closure = {policy.id: policy}
        self.addclosure(policy, closure, [policy.id])
        return closure

    def addclosure(self, policy, closure, path):
        for subpolicy in policy.subpolicies:
            ssid = subpolicy.policy_id
            assert ssid not in path, ssid
            if ssid not in closure:
                ss = self.getpolicy(ssid)
                if ss is None and isinstance(ssid, tuple):
                    continue
                assert ss is not None, ssid
                assert ssid not in closure, ssid
                closure[ssid] = ss
                self.addclosure(ss, closure, path + [ssid])

    def check(self, templates):
        assert isinstance(templates, Templates), templates
        idmap = {}
        ridmap = {}
        namemap = {}
        namedups = {}
        for s in self.policies:
            if s.name not in namemap:
                namemap[s.name] = [s.id]
            else:
                L = namemap[s.name]
                L.append(s.id)
                namedups[s.name] = L
            s.check(self, templates)
            assert s.id not in idmap, s.id
            idmap[s.id] = s
            for r in s.rules:
                assert r.id not in ridmap, r.id
                ridmap[r.id] = r
            self.getclosure(s) # raises AssertionError on cycles
        assert not namedups, _formatnamedups(namedups, "policy")

class Module(ElementClass):
    __element__ = "module"
    __attributes__ = {"name": String}
    __characters__ = "text"

    def getprocedure(self, procedure):
        """Get information about a given procedure.

        Returns a possibly empty list of (name, type, default) triples
        describing the procedure's formal parameters (excluding the
        initial 'NAME: Host' argument).  The default is None if no
        default value was present, otherwise a string representing the
        Fuel expression used for the default value.  The name and type
        are strings representing identifiers.  The type may also be of
        the form 'set of X' where X is an identifier. (Only one level
        of 'set of' is supported.)

        If a parsing error occurs before the end of the parameter
        list, the return value is None.  (Note the difference between
        [] and None!!!)

        None is also returned if the module doesn't define the given
        procedure.

        XXX The parser can be fooled by something looking like the
        start of a procedure definition occurring at the start of a
        line inside a multi-line string.  Fixing that would be
        possible but expensive (parsing the file up till the start of
        the requested procedure would become much slower) so I'd
        rather not do this; I'll just request that Fuel programmers
        don't do that.  (Indenting the decoy procedure definition is
        enough to hide it from the parser.)

        """
        # Look for the start of the requested procedure using a
        # regular expression.
        p = re.escape(procedure)
        regex = re.compile(r"^proc\s+(%s)\s*\(\s*\w+\s*:\s*Host\s*[,)]" % p)
        # XXX Alternatively, could use StringIO -- which way is faster?
        lines = self.text.splitlines(True)
        for i, line in enumerate(lines):
            if regex.match(line):
                break
        else:
            return None
        # Now parse from lines[i] onwards until the end of the
        # parameter list.  We expect a simplified grammar:
        # 'proc' NAME '(' NAME ':' 'Host'
        #                 (',' NAME ':' ['set' 'of'] NAME ['=' Value)*a
        #             ')' ':'
        # Comments are allowed anywhere and ignored.  For Value we
        # take pretty much any sequence of tokens with balanced
        # parentheses and not containing ',' or ')' or '=' or ':' or
        # ';' at the outermost level.
        del lines[:i]
        it = iter(lines)
        def readline():
            # This is the interface that generate_tokens() needs.
            try:
                return next(it)
            except StopIteration:
                return ""
        def simplify(gen):
            # A generator that takes 5-tuples as produced by tokenize;
            # it strips comments and whitespace and only returns the
            # token string (ignoring the metadata that
            # generate_tokens() helpfully supplies).
            for t in gen:
                token = t[1]
                if not (token.startswith("#") or token.isspace()):
                    yield token
        tokens = simplify(tokenize.generate_tokens(readline))
        _next_token = tokens.__next__  # Python 3: use __next__ bound method
        def check(expected):
            # Helper to check that an expected token actually occurs;
            # raises StopIteration if not.  This consumes the next
            # token.
            tok = _next_token()
            if tok != expected:
                ##print("got %r, expected %r" % (tok, expected))
                raise StopIteration
        # Big hand-written parser for the grammar given above.
        try:
            check("proc")
            check(procedure)
            check("(")
            arg0 = _next_token()
            check(":")
            check("Host")
            tok = _next_token()
            params = [] # List of (name, type, default) triples
            # Now loop as long as there are parameters.
            while tok == ",":
                name = _next_token()
                check(":")
                type = _next_token()
                if type == "set":
                    # Allow only a single "set of" prefix.
                    check("of")
                    type = "set of " + _next_token()
                tok = _next_token()
                default = None
                if tok == "=":
                    words = []
                    stack = []
                    tok = _next_token()
                    # Loop until we find a terminator outside matching
                    # parentheses.  A few terminators are actually
                    # errors, we stop at these to detect errors (like
                    # a missing final close parenthesis) earlier
                    # rather than later.
                    while stack or tok not in (")", ",", "=", ":", ";"):
                        if tok == "(":
                            stack.append(")")
                        elif tok == "[":
                            stack.append("]")
                        elif tok == "{":
                            stack.append("}")
                        elif tok in (")", "]", "}"):
                            if not stack or stack.pop() != tok:
                                raise StopIteration
                        words.append(tok)
                        tok = _next_token()
                    default = " ".join(words)
                params.append((name, type, default))
            if tok != ")":
                raise StopIteration
            check(":")
            return params
        except StopIteration:
            return None # Parse failed

class Modules(ElementClass):
    __element__ = "modules"
    __children__ = {Module: "modules[]"}

    def get(self, name):
        for module in self.modules:
            if module.name == name:
                return module
        return None

class Dependency(ElementClass):
    __element__ = "dependency"
    __attributes__ = {"id": Integer,
                      "name": String,
                      "short": String,
                      "min-version": Integer}

class Dependencies(ElementClass):
    __element__ = "dependencies"
    __children__ = {Dependency: "dependencies[]"}

class FuelPack(ElementClass):
    __element__ = "fuelpack"
    __attributes__ = {"id": Integer,
                      "name": String,
                      "short": String,
                      "version": Integer,
                      "timestamp": Timestamp,
                      "parent": IdRef}
    __children__ = {Dependencies: "dependencies",
                    Policies: "policies",
                    Templates: "templates",
                    Modules: "modules"}

    # XXX These numerical assignments are historical; see getcatmap()
    catmap = {
        'accounts':        1,
        'network':         3,
        'filesystem':      4,
        'system':          5,
        'hardware':        6,
        'software':        7,
        'packet filter':   8,
        'trust':          11,
        'authentication': 12,
        'gather':         13,
        'logging':        14,
        'container':      15,
        }

    def getcatmap(self):
        # XXX The categories used to be stored inside rules.xml; now
        # they are hardcoded.  At some point they should probably be
        # read from a separate file (though arguably this module is a
        # separate file too :-).  Sorry, Jerry!
        return self.catmap

    def check(self):
        self.templates.check(self.getcatmap())
        self.policies.check(self.templates)

    fpresolver = None # fpresolver(fpid, basefp) -> FuelPack

    def setfpresolver(self, fpresolver):
        self.fpresolver = fpresolver

    def getfp(self, id):
        if self.fpresolver is not None:
            return self.fpresolver(id, self)
        else:
            return None

    def splitid(self, id):
        if isinstance(id, tuple):
            fpid, id = id
            fp = self.getfp(fpid)
        else:
            fp = self
        return fp, id

    def gettemplate(self, id):
        fp, id = self.splitid(id)
        if fp is None:
            return None
        return fp.templates.get(id)

    def getpolicy(self, id):
        fp, id = self.splitid(id)
        if fp is None:
            return None
        return fp.policies.getpolicy(id)

    def getrule(self, id):
        return self.policies.getrule(id)

    def getmodule(self, name):
        return self.modules.get(name)

    def getprocedure(self, name, procedure):
        """Get information about a given procedure.

        Arguments: the module and procedure name.

        Returns: see Module.getprocedure().  None is also returned if
        the module is not found.  There are no diagnostics.

        """
        mod = self.modules.get(name)
        if mod is None:
            return None
        return mod.getprocedure(procedure)

    def getpolicybyname(self, name):
        return self.policies.getpolicybyname(name)

    def gettemplatebyname(self, name):
        return self.templates.gettemplatebyname(name)

    def next_policy_id(self):
        return self.policies.next_policy_id()

    def next_rule_id(self):
        return self.policies.next_rule_id()

    def next_template_id(self):
        return self.templates.next_template_id()

    def newtemplate(self, name, id=None):
        if id is None:
            id = self.next_template_id()
        tmpl = Template(id=id, name=name,
                        description=Description())
        if self.using_parents:
            tmpl.parents = []
        self.templates.templates.append(tmpl)
        return tmpl

    def newpolicy(self, name, id=None):
        if id is None:
            id = self.next_policy_id()
        policy = Policy(id=id, name=name, description=Description())
        if self.using_parents:
            policy.parents = []
        self.policies.policies.append(policy)
        return policy

    def deletetemplate(self, tmpl):
        assert self.using_parents, "deletetemplate() needs addparentage()"
        if tmpl.parents != []:
            raise ReferentialIntegrityError()
        assert tmpl in self.templates.templates
        self.templates.templates.remove(tmpl)

    def deletepolicy(self, policy):
        assert self.using_parents, "deletepolicy() needs addparentage()"
        if policy.parents != []:
            raise ReferentialIntegrityError()
        assert policy in self.policies.policies
        self.policies.policies.remove(policy)
        for ss in policy.subpolicies:
            fp, id = self.splitid(ss.policy_id)
            subpolicy = fp.getpolicy(id)
            assert subpolicy is not None, (policy.id, ss.policy_id)
            if fp is self:
                assert policy in subpolicy.parents, (policy, subpolicy.parents)
                subpolicy.parents.remove(policy)
                if subpolicy.parents == []:
                    self.deletepolicy(subpolicy)
        del policy.subpolicies[:]
        for rule in policy.rules:
            fp, id = self.splitid(rule.template_id)
            tmpl = fp.gettemplate(id)
            assert tmpl is not None, (policy.id, rule.template_id)
            if fp is self:
                assert policy in tmpl.parents, (policy.id, tmpl.id)
                tmpl.parents.remove(policy)
                if tmpl.parents == []:
                    self.deletetemplate(tmpl)
        del policy.rules[:]

    def newrule(self, policy, tmpl, fp=None):
        if fp is None:
            fp = self
        assert isinstance(policy, Policy)
        assert isinstance(tmpl, Template)
        assert policy in self.policies.policies
        assert tmpl in fp.templates.templates
        tid = tmpl.id
        if fp is not self:
            tid = (fp.id, tid)
        rule = Rule(id=self.next_rule_id(), template_id=tid)
        if fp is self and self.using_parents:
            tmpl.parents.append(policy)
        policy.rules.append(rule)
        return rule

    def newsubpolicy(self, parent, child, fp=None):
        if fp is None:
            fp = self
        assert isinstance(parent, Policy)
        assert isinstance(child, Policy)
        assert parent in self.policies.policies
        assert child in fp.policies.policies
        cid = child.id
        if fp is not self:
            cid = (fp.id, cid)
        else:
            if self.using_parents:
                if child.id in parent.getancestors():
                    raise CycleError("newsubpolicy() won't create cycles")
            else:
                closure = self.policies.getclosure(child)
                if parent.id in closure:
                    raise CycleError("newsubpolicy() won't create cycles")
        for ss in parent.subpolicies:
            if ss.policy_id == cid:
                raise DuplicateError("newsubpolicy() won't create duplicates")
        ss = Subpolicy(policy_id=cid)
        if fp is self and self.using_parents:
            child.parents.append(parent)
        parent.subpolicies.append(ss)
        return ss

    def deleterule(self, policy, tmpl, fp=None):
        # XXX This is wrong for parameterized rules; we should be able
        # to delete a specific rule.  See also moveobjects().
        if fp is None:
            fp = self
        assert isinstance(policy, Policy), repr(policy)
        assert isinstance(tmpl, Template), repr(tmpl)
        assert policy in self.policies.policies, (policy.id, policy.name)
        assert tmpl in fp.templates.templates, (tmpl.id, tmpl.name)
        tid = tmpl.id
        if fp is not self:
            tid = (fp.id, tid)
        for rule in policy.rules:
            if rule.template_id == tid:
                policy.rules.remove(rule)
                if fp is self and self.using_parents:
                    assert policy in tmpl.parents, tmpl.parents
                    tmpl.parents.remove(policy)
                break
        else:
            raise MissingError("deleterule() didn't find the rule")

    def deletesubpolicy(self, parent, child, fp=None):
        if fp is None:
            fp = self
        assert isinstance(parent, Policy)
        assert isinstance(child, Policy)
        assert parent in self.policies.policies
        assert child in fp.policies.policies
        cid = child.id
        if fp is not self:
            cid = (fp.id, cid)
        for ss in parent.subpolicies:
            if ss.policy_id == cid:
                parent.subpolicies.remove(ss)
                if fp is self and self.using_parents:
                    assert parent in child.parents, child.parents
                    child.parents.remove(parent)
                break
        else:
            raise MissingError("deletesubpolicy() didn't find the subpolicy")

    def emptytrash(self):
        # Note that objects in the trash may still be referenced elsewhere
        trash = self.getpolicy(TRASH_POLICY_ID)
        if trash is not None and (trash.subpolicies or trash.rules):
            was_using_parents = self.using_parents
            if not was_using_parents:
                self.addparentage()
            for ss in trash.subpolicies[:]:
                fp, id = self.splitid(ss.policy_id)
                policy = fp.getpolicy(id)
                self.deletesubpolicy(trash, policy, fp)
                if fp is self and policy.parents == []:
                    self.deletepolicy(policy)
            for rule in trash.rules[:]:
                fp, id = self.splitid(rule.template_id)
                tmpl = fp.gettemplate(id)
                if tmpl is None:
                    trash.rules.remove(rule)
                else:
                    self.deleterule(trash, tmpl, fp)
                    if fp is self and tmpl.parents == []:
                        self.deletetemplate(tmpl)
            if not was_using_parents:
                self.removeparentage()

    using_parents = False # Are we maintaining 'parents' lists?

    def addparentage(self):
        """Add and fill a 'parents' list to all policies & templates."""
        self.using_parents = True
        for policy in self.policies.policies:
            policy.parents = []
        for policy in self.policies.policies:
            for ss in policy.subpolicies:
                pid = ss.policy_id
                if not isinstance(pid, tuple):
                    subpolicy = self.getpolicy(pid)
                    assert subpolicy is not None, pid
                    subpolicy.parents.append(policy)
        for tmpl in self.templates.templates:
            tmpl.parents = []
        for policy in self.policies.policies:
            for rule in policy.rules:
                tid = rule.template_id
                if not isinstance(tid, tuple):
                    tmpl = self.gettemplate(tid)
                    assert tmpl is not None, tid
                    tmpl.parents.append(policy)

    def removeparentage(self):
        "Remove the 'parents' lists (reset them to None)."
        self.using_parents = False
        for policy in self.policies.policies:
            policy.parents = None
        for tmpl in self.templates.templates:
            tmpl.parents = None

    def isancestor(self, a, b):
        "Return True iff Policy a is an ancestor of Policy b."
        # Implementation strategy: look for a amongst parents of b
        if a is b:
            return True
        assert self.using_parents
        assert isinstance(a, Policy), a
        assert isinstance(b, Policy), b
        for x in b.parents:
            if self.isancestor(a, x):
                return True
        return False

    def moveobjects(self, sourcepairs, target):
        """Move rules/policies to a new parent.

        The sourcepairs argument is a list of (parent, object) tuples
        where each parent is a Policy instance and each object is
        either a Policy or a Rule instance.

        The target argument is a Policy instance.

        """
        assert self.using_parents
        assert isinstance(target, Policy)
        for parent, obj in sourcepairs:
            if parent is target:
                continue # Nothing to do
            assert isinstance(parent, Policy), parent
            if isinstance(obj, Policy):
                assert parent in obj.parents
                assert not self.isancestor(obj, target)
                self.deletesubpolicy(parent, obj)
                self.newsubpolicy(target, obj)
            else:
                assert isinstance(obj, Rule)
                assert obj in parent.rules
                tmpl = self.gettemplate(obj.template_id)
                assert parent in tmpl.parents
                self.deleterule(parent, tmpl)
                target.rules.append(obj)
                tmpl.parents.append(target)
        # Normalize target lists
        target.rules.sort(key=lambda a: a.id)
        target.subpolicies.sort(key=lambda a: a.policy_id)

    def checkparams(self, params, impls):
        """Cross-check parameters with implementations.

        Returns None when all is well.

        Returns a triple (message, param, impl) when there are errors;
        message is a string and param and impl may be None or are the
        parameter and/or implementation implicated in the error.

        """
        for impl in impls:
            # Set iparams to a list of (name, type, default) triples.
            mod = self.getmodule(impl.module)
            if mod is None:
                return ("Can't find module %s" % impl.module, None, impl)
            iparams = mod.getprocedure(impl.procedure)
            if iparams is None:
                return ("Can't find procedure %s" % impl.procedure, None, impl)
            # There can't be more parameters in the template than in
            # the procedure declaration.
            if len(params) > len(iparams):
                return ("There are more parameters (%d) than declared (%d)" %
                        (len(params), len(iparams)), None, impl)
            if len(params) < len(iparams):
                # The remaining iparams must all have defaults
                for name, type, default in iparams[len(params):]:
                    if default is None:
                        return ("The required parameter (%s: %s) is missing" %
                                (name, type), None, impl)
            for p, ip in zip(params, iparams):
                name, type, default = ip
                error = None
                if p.name != name or p.type != type:
                    return ("Given parameter doesn't match declared parameter "
                            "(%s: %s)" % (name, type), p, impl)
            # XXX Should we cross-check the default?  Not for now.
        return None

def load_fuelpack(file):
    """Reads the whole structure from a file (stream or filename).

    Also sets the convenience attributes _root and _trash.

    """
    fp = FuelPack.__parseFile__(file) 
    fp._root = fp.getpolicy(ROOT_POLICY_ID)
    fp._trash = fp.getpolicy(TRASH_POLICY_ID)
    return fp

def new_fuelpack():
    """Creates an anonymous, minimal Fuel Pack with Root and Trash policies.

    Also sets the convenience attributes _root and _trash.

    """
    fp = FuelPack(policies=Policies(), templates=Templates(),
                  modules=Modules())
    fp._root = fp.newpolicy("Root", ROOT_POLICY_ID)
    fp._trash = fp.newpolicy("Trash", TRASH_POLICY_ID)
    fp.newsubpolicy(fp._root, fp._trash)
    return fp

def all_fuelpacks():
    """Returns a list with the filenames of all Fuel Packs.

    This only searches the standard location (agent/fuelpacks).

    """
    import glob
    dn = os.path.dirname(__file__)
    dn = os.path.join(dn, os.pardir, os.pardir, os.pardir)
    dn = os.path.normpath(dn)
    dn = os.path.join(dn, "fuelpacks")
    pat = os.path.join(dn, "*.xml")
    args = glob.glob(pat)
    args.sort()
    return args
