Index: smart/channels/yast2.py
===================================================================
--- smart/channels/yast2.py	(.../trunk)	(revision 0)
+++ smart/channels/yast2.py	(.../branches/yast2-channel)	(revision 695)
@@ -0,0 +1,119 @@
+#
+# Copyright (c) 2004 Conectiva, Inc.
+#
+# Written by Mauricio Teixeira <mteixeira@webset.net>
+#
+# This file is part of Smart Package Manager.
+#
+# Smart Package Manager 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.
+#
+# Smart Package Manager 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 Smart Package Manager; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+from smart.backends.rpm.yast2 import YaST2Loader
+from smart.util.filetools import getFileDigest
+from smart.const import SUCCEEDED, FAILED, NEVER
+from smart.channel import PackageChannel
+from smart import *
+import posixpath
+import tempfile
+import commands
+import os
+
+class YaST2Channel(PackageChannel):
+    def __init__(self, baseurl, *args):
+        super(YaST2Channel, self).__init__(*args)
+        self._baseurl = baseurl
+
+    def getCacheCompareURLs(self):
+	return [posixpath.join(self._baseurl, "media.1/media")]
+
+    def getFetchSteps(self):
+        return 4
+
+    def __fetchFile(self, file, fetcher, progress):
+        fetcher.reset()
+        item = fetcher.enqueue(file)
+        fetcher.run(progress=progress)
+        failed = item.getFailedReason()
+        if failed:
+            progress.add(self.getFetchSteps()-1)
+            progress.show()
+            if fetcher.getCaching() is NEVER:
+                lines = [_("Failed acquiring information for '%s':") % self,
+                         "%s: %s" % (item.getURL(), failed)]
+                raise Error, "\n".join(lines)
+            return False
+        return item
+
+    def fetch(self, fetcher, progress):
+
+        # Fetch media information file
+        # This file contains the timestamp info
+        # that says if the repository has changed
+        fetchitem = posixpath.join(self._baseurl, "media.1/media")
+        fetched = self.__fetchFile(fetchitem, fetcher, progress)
+        if not fetched: return False
+
+        digest = getFileDigest(fetched.getTargetPath())
+        #if digest == self._digest and getattr(self, "force-yast", False):
+        if digest == self._digest:
+            return True
+        self.removeLoaders()
+
+        # Find location of description files
+        fetchitem = posixpath.join(self._baseurl, "content")
+        fetched = self.__fetchFile(fetchitem, fetcher, progress)
+        if not fetched: return False
+        self.removeLoaders()
+        descrdir = "suse/setup/descr"
+        datadir = "RPMS"
+        for line in open(fetched.getTargetPath()):
+            if line.startswith("DESCRDIR"): descrdir = line[9:-1]
+            if line.startswith("DATADIR"): datadir = line[8:-1]
+
+        # Fetch package information (req, dep, prov, etc)
+        fetchitem = posixpath.join(self._baseurl,
+                                  (("%s/packages") % descrdir))
+        fetched = self.__fetchFile(fetchitem, fetcher, progress)
+        if not fetched: return False
+        self.removeLoaders()
+        pkginfofile = fetched.getTargetPath()
+        if open(pkginfofile).read(9) == "=Ver: 2.0":
+            fetchitem = posixpath.join(self._baseurl,
+                                      (("%s/packages.en") % descrdir))
+            fetched = self.__fetchFile(fetchitem, fetcher, progress)
+            if not fetched or open(fetched.getTargetPath()).read(9) != "=Ver: 2.0":
+                raise Error, "YaST2 package descriptions not loaded."
+                loader = YaST2Loader(self._baseurl, datadir, pkginfofile)
+            else:
+                pkgdescfile = fetched.getTargetPath()
+                loader = YaST2Loader(self._baseurl, datadir, pkginfofile, pkgdescfile)
+            loader.setChannel(self)
+            self._loaders.append(loader)
+        else:
+            raise Error, _("Invalid package file format. Invalid header found.")
+
+        self._digest = digest
+
+        return True
+
+def create(alias, data):
+    return YaST2Channel(data["baseurl"],
+                        data["type"],
+                        alias,
+                        data["name"],
+                        data["manual"],
+                        data["removable"],
+                        data["priority"])
+
+# vim:ts=4:sw=4:et
Index: smart/channels/yast2_info.py
===================================================================
--- smart/channels/yast2_info.py	(.../trunk)	(revision 0)
+++ smart/channels/yast2_info.py	(.../branches/yast2-channel)	(revision 695)
@@ -0,0 +1,35 @@
+#
+# Copyright (c) 2004 Conectiva, Inc.
+#
+# Written by Mauricio Teixeira <mteixeira@webset.net>
+#
+# This file is part of Smart Package Manager.
+#
+# Smart Package Manager 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.
+#
+# Smart Package Manager 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 Smart Package Manager; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+from smart import _
+
+kind = "package"
+
+name = _("YaST2 Repository")
+
+description = _("""
+Repositories created for YaST2.
+""")
+
+fields = [("baseurl", _("Base URL"), str, None,
+           _("Base URL of YaST2 repository, where directory.yast is located.")),
+           ("medias", _("Medias"), str, "",
+           _("Space separated list of medias. (NOT IN USE YET)"))]
Index: smart/backends/rpm/yast2.py
===================================================================
--- smart/backends/rpm/yast2.py	(.../trunk)	(revision 0)
+++ smart/backends/rpm/yast2.py	(.../branches/yast2-channel)	(revision 695)
@@ -0,0 +1,282 @@
+#
+# Copyright (c) 2004 Conectiva, Inc.
+#
+# Written by Mauricio Teixeira <mteixeira@webset.net>
+#
+# This file is part of Smart Package Manager.
+#
+# Smart Package Manager 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.
+#
+# Smart Package Manager 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 Smart Package Manager; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+from smart.backends.rpm.rpmver import splitarch
+from smart.cache import PackageInfo, Loader
+from smart.backends.rpm.base import *
+from smart import *
+import posixpath
+import locale
+import os
+
+class YaST2PackageInfo(PackageInfo):
+    def __init__(self, package, loader, info):
+        PackageInfo.__init__(self, package)
+        self._loader = loader
+        self._info = info
+
+    def getURLs(self):
+        version, arch = splitarch(self._package.version)
+        return [posixpath.join(self._loader._baseurl,
+                               self._loader._datadir, arch,
+                               self._info.get("filename"))]
+
+    def getInstalledSize(self):
+        return int(self._info.get("instsize"))
+
+    def getSize(self, url):
+        return int(self._info.get("size"))
+                
+    def getSummary(self):
+        return self._info.get("summary", "")
+
+    def getDescription(self):
+        return self._info.get("description", "")
+
+    def getGroup(self):
+        return self._info.get("group", "")
+
+class YaST2Loader(Loader):
+
+    def __init__(self, baseurl, datadir, pkginfofile, pkgdescfile=None):
+        Loader.__init__(self)
+        self._baseurl = baseurl
+        self._datadir = datadir
+        self._pkginfofile = pkginfofile
+        self._pkgdescfile = pkgdescfile
+
+    def getInfo(self, pkg):
+        return YaST2PackageInfo(pkg, self, pkg.loaders[self])
+
+    def getLoadSteps(self):
+        pkgfile = open(self._pkginfofile)
+        total = 0
+        for line in pkgfile:
+            if line.startswith("=Pkg: "):
+                total += 1
+        pkgfile.close()
+        return total
+
+    def getInfoEntity(self, tag):
+        data = []
+        found = False
+        for line in self._pkgentry:
+            if line.startswith("+" + tag + ":"):
+                found = True
+                continue
+            elif line.startswith("-" + tag + ":"):
+                break
+            elif line[:7] in ("rpmlib(", "config("):
+                continue
+            elif found == True:
+                parts = line.split(" ")
+                if len(parts) == 1:
+                    data.append((line, None, None))
+                else:
+                    data.append((parts[0], parts[1], parts[2]))
+        return data
+
+    def stripTags(self, s):
+        # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/440481
+        # this list is neccesarry because chk() would otherwise not know
+        # that intag in stripTags() is ment, and not a new intag variable in chk().
+        intag = [False]
+        def chk(c):
+            if intag[0]:
+                intag[0] = (c != '>')
+                return False
+            elif c == '<':
+                intag[0] = True
+                return False
+            return True
+        return ''.join(c for c in s if chk(c))
+
+    def readPkgSummDesc(self, entryname):
+        summary = description = ""
+        if self._pkgdescfile and self._pkgoffsets.has_key(entryname):
+            self._descfile.seek((self._pkgoffsets[entryname] + 1))
+            summary = self._descfile.readline()[5:-1]
+            description = ""
+            # WARNING - this could cause an infinite loop!
+            # We need to find a better way to do it!
+            while 1:
+                line = self._descfile.readline()
+                if line.startswith("+Des:") or line.startswith("<!--"): continue
+                if not line or line[:-1] == "-Des:": break
+                description += self.stripTags(line[:-1])
+        return summary, description
+
+
+    def parseEntry(self):
+        Pkg = RPMPackage
+        Req = RPMRequires
+        Prq = RPMPreRequires
+        Prv = RPMProvides
+        Con = RPMConflicts
+        Obs = RPMObsoletes
+        NPrv = RPMNameProvides
+        # SUSE fields not yet handled
+        # Rec / Sug / Src / Aut / Srh / Key
+
+        requires = ()
+        provides = ()
+        conflicts = ()
+        obsoletes = ()
+
+        # avoid parsing errors on undefined fields
+        requires = prequire = conflicts = obsoletes = []
+
+        for line in self._pkgentry:
+            kw = line[:4]
+            if kw == "=Pkg":
+                entryname = line[6:]
+                nameparts = entryname.split(" ")
+                # skip entry if arch is not compatible
+                arch = nameparts[3]
+                if rpm.archscore(arch) <= 0:
+                    return
+                name = nameparts[0]
+                version = nameparts[1]
+                release = nameparts[2]
+                versionarch = "%s-%s@%s" % \
+                               (version, release, arch)
+            elif kw == "+Req":
+                requires = self.getInfoEntity("Req")
+            elif kw == "+Prq":
+                prequire = self.getInfoEntity("Prq")
+            elif kw == "+Prv":
+                provides = self.getInfoEntity("Prv")
+            elif kw == "+Con":
+                conflicts = self.getInfoEntity("Con")
+            elif kw == "+Obs":
+                obsoletes = self.getInfoEntity("Obs")
+            elif kw == "=Loc":
+                locparts = line[6:].split(" ")
+                media = locparts[0]
+                filename = locparts[1]
+            elif kw == "=Siz":
+                sizeparts = line[6:].split(" ")
+                size = sizeparts[0]
+                instsize = sizeparts[1]
+            elif kw == "=Grp":
+                group = line[6:]
+
+        summary, description = self.readPkgSummDesc(entryname)
+        info = { "summary"     : summary,
+                 "description" : description,
+                 "size"        : size,
+                 "instsize"    : instsize,
+                 "group"       : group,
+                 "media"       : media,
+                 "filename"    : filename }
+                 
+        prvdict = {}
+        for n, r, v in provides:
+            if n == name and v == version + "-" + release:
+                prv = (NPrv, n, versionarch)
+            else:
+                prv = (Prv, n, v)
+            prvdict[prv] = True
+
+        reqdict = {}
+        for n, r, v in requires:
+            if ((r is not None and r != "=") or
+                ((Prv, n, v) not in prvdict)):
+                    reqdict[(Req, n, r, v)] = True
+        for n, r, v in prequire:
+            if ((r is not None and r != "=") or
+                ((Prq, n, v) not in prvdict)):
+                    reqdict[(Prq, n, r, v)] = True
+
+        cnfdict = {}
+        for n, r, v in conflicts:
+            cnfdict[(Con, n, r, v)] = True
+                        
+        upgdict = {}
+        upgdict[(Obs, name, "<", versionarch)] = True
+                   
+        for n, r, v in obsoletes:
+            upg = (Obs, n, r, v)
+            upgdict[upg] = True
+            cnfdict[upg] = True
+                    
+        pkg = self.buildPackage((Pkg, name, versionarch),
+                                prvdict.keys(), reqdict.keys(),
+                                upgdict.keys(), cnfdict.keys())
+                    
+        pkg.loaders[self] = info
+                    
+
+    def load(self):
+
+        prog = iface.getProgress(self._cache)
+
+        try:
+            self._infofile = open(self._pkginfofile)
+        except (IOError, OSError), e:
+            raise Error, "Error opening package information file. %s", e
+
+        if self._pkgdescfile:
+            try:
+                self._descfile = open(self._pkgdescfile)
+                # populate pointers for lines that matches packages on description file
+                self._pkgoffsets = {}
+                # WARNING - this could cause an infinite loop!
+                # We need to find a better way to do it!
+                while 1:
+                    line = self._descfile.readline()
+                    if not line: break
+                    if line[:6] == "=Pkg: ":
+                        self._pkgoffsets[line[6:-1]] = self._descfile.tell()
+            except (IOError, OSError), e:
+                pass
+
+        while 1:
+            line = self._infofile.readline()
+            if not line: break
+            if line == "=Ver: 2.0\n": continue
+            # Read a full package entry
+            if line[:4] == ("=Pkg"):
+                self._pkgentry = []
+                self._pkgentry.append(line[:-1])
+                while 1:
+                    eline = self._infofile.readline()[:-1]
+                    if not eline: break
+                    if eline[:4] == ("##--"): break
+                    self._pkgentry.append(eline)
+                # Parse the entry
+                self.parseEntry()
+                prog.add(1)
+                prog.show()
+
+        self._pkgoffsets = {}
+        self._infofile.close()
+        self._descfile.close()
+
+def enablePsyco(psyco):
+        psyco.bind(YaST2Loader.getInfoEntity)
+        psyco.bind(YaST2Loader.readPkgSummDesc)
+        psyco.bind(YaST2Loader.load)
+
+hooks.register("enable-psyco", enablePsyco)
+            
+
+# vim:ts=4:sw=4:et
Index: tests/yast2.txt
===================================================================
--- tests/yast2.txt	(.../trunk)	(revision 0)
+++ tests/yast2.txt	(.../branches/yast2-channel)	(revision 695)
@@ -0,0 +1,95 @@
+
+Create the channel.
+
+  >>> from smart.channel import createChannel
+  >>> channel = createChannel("alias",
+  ...                         {"type": "yast2",
+  ...                          "baseurl": "file://%s/yast2" % TESTDATADIR})
+  >>> channel
+  <smart.channels.yast2.YaST2Channel object at ...>
+
+
+We need a progress and a fetcher.
+
+  >>> from smart.progress import Progress
+  >>> from smart.fetcher import Fetcher
+  >>> progress = Progress()
+  >>> fetcher = Fetcher()
+
+
+Fetch channel data.
+
+  >>> channel.fetch(fetcher, progress)
+  True
+  >>> channel.getLoaders()
+  [<smart.backends.rpm.yast2.YaST2Loader object at ...>]
+
+
+Let's create a cache to put the loader in, so that we can test it.
+
+  >>> from smart.cache import Cache
+  >>> loader = channel.getLoaders()[0]
+  >>> cache = Cache()
+  >>> cache.addLoader(loader)
+
+
+The setup is ready. Now we can load the data into the cache.
+
+  >>> cache.load()
+  Updating cache...               ######################################## [100%]
+  <BLANKLINE>
+
+
+This should give us one package with the data we already know.
+
+  >>> cache.getPackages()
+  [name1-version1-release1@noarch, name2-version2-release2@noarch]
+
+  >>> pkg = cache.getPackages()[0]
+  >>> type(pkg)
+  <class 'smart.backends.rpm.base.RPMPackage'>
+
+
+Let's inspect the package data.
+
+  >>> pkg.name
+  'name1'
+  >>> pkg.version
+  'version1-release1@noarch'
+
+  >>> sorted(pkg.provides)
+  [name1 = version1-release1@noarch, providename1 = provideversion1]
+  >>> [type(x).__name__ for x in sorted(pkg.provides)]
+  ['RPMNameProvides', 'RPMProvides']
+
+  >>> sorted(pkg.requires)
+  [/bin/sh, prerequirename1 = prerequireversion1, requirename1 = requireversion1]
+  >>> # XXX pre-requires are broken in createrepo
+  >>> [type(x).__name__ for x in sorted(pkg.requires)]
+  ['RPMRequires', 'RPMRequires', 'RPMRequires']
+
+  >>> sorted(pkg.upgrades)
+  [name1 < version1-release1@noarch, obsoletesname1 = obsoletesversion1]
+  >>> [type(x).__name__ for x in sorted(pkg.upgrades)]
+  ['RPMObsoletes', 'RPMObsoletes']
+
+  >>> sorted(pkg.conflicts)
+  [conflictsname1 = conflictsversion1, obsoletesname1 = obsoletesversion1]
+  >>> [type(x).__name__ for x in sorted(pkg.conflicts)]
+  ['RPMConflicts', 'RPMObsoletes']
+
+
+Now let's ask the loader for a PackageInfo instance, and inspect it.
+
+  >>> info = loader.getInfo(pkg)
+  >>> info
+  <smart.backends.rpm.yast2.YaST2PackageInfo object at ...>
+
+  >>> info.getGroup()
+  'Group1'
+  >>> info.getSummary()
+  'Summary1'
+  >>> info.getDescription()
+  'Description1'
+
+vim:ft=doctest
Index: tests/data/yast2/setup/descr/packages.en
===================================================================
--- tests/data/yast2/setup/descr/packages.en	(.../trunk)	(revision 0)
+++ tests/data/yast2/setup/descr/packages.en	(.../branches/yast2-channel)	(revision 695)
@@ -0,0 +1,13 @@
+=Ver: 2.0
+##----------------------------------------
+=Pkg: name1 version1 release1 noarch
+=Sum: Summary1
++Des:
+Description1
+-Des:
+##----------------------------------------
+=Pkg: name2 version2 release2 noarch
+=Sum: Summary2
++Des:
+Description2
+-Des:
Index: tests/data/yast2/setup/descr/packages.DU
===================================================================
--- tests/data/yast2/setup/descr/packages.DU	(.../trunk)	(revision 0)
+++ tests/data/yast2/setup/descr/packages.DU	(.../branches/yast2-channel)	(revision 695)
@@ -0,0 +1,13 @@
+=Ver: 2.0
+##----------------------------------------
+=Pkg: name1 version1 release1 noarch
++Dir:
+/ 0 1 0 1
+tmp/ 1 0 1 0
+-Dir:
+##----------------------------------------
+=Pkg: name2 version2 release2 noarch
++Dir:
+/ 0 1 0 1
+tmp/ 1 0 1 0
+-Dir:
Index: tests/data/yast2/setup/descr/packages
===================================================================
--- tests/data/yast2/setup/descr/packages	(.../trunk)	(revision 0)
+++ tests/data/yast2/setup/descr/packages	(.../branches/yast2-channel)	(revision 695)
@@ -0,0 +1,55 @@
+=Ver: 2.0
+##----------------------------------------
+=Pkg: name1 version1 release1 noarch
++Req:
+/bin/sh
+/bin/sh
+prerequirename1 = prerequireversion1
+requirename1 = requireversion1
+rpmlib(CompressedFileNames) <= 3.0.4-1
+rpmlib(PayloadFilesHavePrefix) <= 4.0-1
+rpmlib(VersionedDependencies) <= 3.0.3-1
+-Req:
++Prv:
+providename1 = provideversion1
+name1 = version1-release1
+-Prv:
++Con:
+conflictsname1 = conflictsversion1
+-Con:
++Obs:
+obsoletesname1 = obsoletesversion1
+-Obs:
+=Grp: Group1
+=Lic: License1
+=Src: name1 version1 release1 src
+=Tim: 1135342886
+=Loc: 1 name1-version1-release1.noarch.rpm
+=Siz: 2160 0
+##----------------------------------------
+=Pkg: name2 version2 release2 noarch
++Req:
+/bin/sh
+/bin/sh
+prerequirename1 = prerequireversion2
+requirename2 = requireversion2
+rpmlib(CompressedFileNames) <= 3.0.4-1
+rpmlib(PayloadFilesHavePrefix) <= 4.0-1
+rpmlib(VersionedDependencies) <= 3.0.3-1
+-Req:
++Prv:
+providename2 = provideversion2
+name2 = version2-release2
+-Prv:
++Con:
+conflictsname2 = conflictsversion2
+-Con:
++Obs:
+obsoletesname2 = obsoletesversion2
+-Obs:
+=Grp: Group2
+=Lic: License2
+=Src: name2 version2 release2 src
+=Tim: 1135342922
+=Loc: 1 name2-version2-release2.noarch.rpm
+=Siz: 2160 0
Index: tests/data/yast2/RPMS/noarch/name1-version1-release1.noarch.rpm
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream

Property changes on: tests/data/yast2/RPMS/noarch/name1-version1-release1.noarch.rpm
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Index: tests/data/yast2/RPMS/noarch/name2-version2-release2.noarch.rpm
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream

Property changes on: tests/data/yast2/RPMS/noarch/name2-version2-release2.noarch.rpm
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Index: tests/data/yast2/directory.yast
===================================================================
--- tests/data/yast2/directory.yast	(.../trunk)	(revision 0)
+++ tests/data/yast2/directory.yast	(.../branches/yast2-channel)	(revision 695)
@@ -0,0 +1,6 @@
+content
+directory.yast
+media.1
+RPMS
+setup
+suse
Index: tests/data/yast2/content
===================================================================
--- tests/data/yast2/content	(.../trunk)	(revision 0)
+++ tests/data/yast2/content	(.../branches/yast2-channel)	(revision 695)
@@ -0,0 +1,9 @@
+PRODUCT tests
+VERSION 10.0-0
+LABEL tests (SMART)
+VENDOR Smart Package Manager
+ARCH.i686 i686 i586 i486 i386 noarch
+ARCH.i586 i586 i486 i386 noarch
+DEFAULTBASE i586
+DESCRDIR setup/descr
+DATADIR RPMS
Index: tests/data/yast2/media.1/media
===================================================================
--- tests/data/yast2/media.1/media	(.../trunk)	(revision 0)
+++ tests/data/yast2/media.1/media	(.../branches/yast2-channel)	(revision 695)
@@ -0,0 +1,3 @@
+tests
+20060109160652
+1
