--- lib/puppet/provider/package/freebsd.rb.orig	Thu Nov  3 10:58:56 2011
+++ lib/puppet/provider/package/freebsd.rb	Thu Nov  3 10:59:02 2011
@@ -1,37 +1,165 @@
-Puppet::Type.type(:package).provide :freebsd, :parent => :openbsd do
-  desc "The specific form of package management on FreeBSD.  This is an
-    extremely quirky packaging system, in that it freely mixes between
-    ports and packages.  Apparently all of the tools are written in Ruby,
-    so there are plans to rewrite this support to directly use those
-    libraries."
+require 'open-uri'
+require 'net/ftp'
+require 'bzip2'
 
-  commands :pkginfo => "/usr/sbin/pkg_info",
-    :pkgadd => "/usr/sbin/pkg_add",
-    :pkgdelete => "/usr/sbin/pkg_delete"
+Puppet::Type.type(:package).provide :freebsd, :parent => Puppet::Provider::Package do
+  include Puppet::Util::Execution
 
+  desc "The specific form of package management on FreeBSD. Resource names must be
+  specified as the port origin: <port_category>/<port_name>."
+
+  commands :pkginfo    => "/usr/sbin/pkg_info",
+           :pkgadd     => "/usr/sbin/pkg_add",
+           :pkgdelete  => "/usr/sbin/pkg_delete"
+
   confine :operatingsystem => :freebsd
+  defaultfor :operatingsystem => :freebsd
 
-  def self.listcmd
-    command(:pkginfo)
+  @@lock = Mutex.new
+  @@ports_index = nil
+
+  # fix bug in URI::FTP merge method that tries to set typecode
+  # even when other is a string.
+  class URI::FTP
+    def merge(other)
+      tmp = super(other)
+      if self != tmp
+        tmp.set_typecode(other.typecode) rescue NoMethodError
+      end
+      return tmp
+    end
   end
 
-  def install
-    should = @resource.should(:ensure)
+  def self.parse_pkg_string(pkg_string)
+    {
+      :pkg_name => pkg_string.split("-").slice(0..-2).join("-"),
+      :pkg_version => pkg_string.split("-")[-1],
+    }
+  end
 
-    if @resource[:source] =~ /\/$/
-      if @resource[:source] =~ /^(ftp|https?):/
-        Puppet::Util::Execution::withenv :PACKAGESITE => @resource[:source] do
-          pkgadd "-r", @resource[:name]
+  def self.unparse_pkg_info(pkg_info)
+    [:pkg_name, :pkg_version].map { |key| pkg_info[key] }.join("-")
+  end
+  
+  def self.parse_origin(origin_path)
+    begin
+      origin = {
+        :port_category => origin_path.split("/").fetch(-2),
+        :port_name     => origin_path.split("/").fetch(-1),
+      }
+    rescue IndexError
+      raise Puppet::Error.new "#{origin_path}: not in required origin format: .*/<port_category>/<port_name>"
+    end
+    origin
+  end
+
+  def self.instances
+    packages = []
+    output = pkginfo "-aoQ"
+    output.split("\n").each do |data|
+      pkg_string, pkg_origin = data.split(":")
+      pkg_info = self.parse_pkg_string(pkg_string)
+
+      packages << new({
+        :provider => self.name,
+        :name     => pkg_origin,
+        :ensure   => pkg_info[:pkg_version],
+      })
+    end
+    packages
+  end
+
+  def ports_index
+    @@lock.synchronize do
+      if @@ports_index.nil?
+        @@ports_index = {}
+        uri = source.merge "INDEX.bz2"
+        Puppet.debug "Fetching INDEX: #{uri.inspect}"
+        begin
+          open(uri, "r") do |f|
+            Bzip2::Reader.open(f.path) do |f|
+              while (line = f.gets)
+                fields = line.split("|")
+                pkg_info = self.class.parse_pkg_string(fields[0])
+                origin = self.class.parse_origin(fields[1])
+                @@ports_index[origin] = pkg_info
+              end
+            end
+          end
+        rescue IOError, OpenURI::HTTPError, Net::FTPError
+          @@ports_index = nil
+          raise Puppet::Error.new "Could not fetch ports INDEX: #{$!}"
         end
-      else
-        Puppet::Util::Execution::withenv :PKG_PATH => @resource[:source] do
-          pkgadd @resource[:name]
+      end
+    end
+    @@ports_index
+  end
+
+  def uri_path
+    Facter.loadfacts
+    File.join(
+      "/", "pub", "FreeBSD", "ports",
+      Facter.value(:hardwareisa),
+      [
+        "packages",
+        Facter.value(:kernelmajversion).split(".")[0],
+        "stable",
+      ].join("-")
+    ) << "/"
+  end
+
+  def source
+    if !defined? @source
+      if @resource[:source]
+        @source = URI.parse(@resource[:source])
+        if @source.path.empty?
+          @source.merge! uri_path
         end
+      else # source parameter not set; build default source URI
+        @source = URI::FTP.build({
+          :host => "ftp.freebsd.org",
+          :path => uri_path,
+        })
       end
+      Puppet.debug "Package: #{@resource[:name]}: source => #{@source.inspect}"
+    end
+    @source
+  end
+
+  def origin
+    if !defined? @origin
+      @origin = self.class.parse_origin(@resource[:name])
+      Puppet.debug "Package: #{@resource[:name]}: origin => #{@origin.inspect}"
+    end
+    @origin
+  end
+
+  def package_uri
+    begin
+      pkg_name = self.class.unparse_pkg_info(ports_index.fetch(origin))
+    rescue IndexError
+      raise Puppet::Error.new "package not found in INDEX"
+    end
+    uri = source.merge File.join("All", pkg_name + ".tbz")
+    Puppet.debug "Package: #{@resource[:name]}: package_uri => #{uri.inspect}"
+    uri
+  end
+
+  def install
+    should = @resource.should(:ensure)
+    origin # call origin so we check the package name for correctness early
+
+    # Source URI is for local file path.
+    if !source.absolute? or source.scheme == "file"
+      pkgadd source.path
+    # Source URI is to specific package file
+    elsif source.absolute? && source.path.end_with?(".tbz")
+      pkgadd source.to_s
+    # Source URI is to a package repository
     else
-      Puppet.warning "source is defined but does not have trailing slash, ignoring #{@resource[:source]}" if @resource[:source]
-      pkgadd "-r", @resource[:name]
+      pkgadd "-f", package_uri.to_s
     end
+    nil
   end
 
   def query
@@ -44,6 +172,7 @@
   end
 
   def uninstall
-    pkgdelete "#{@resource[:name]}-#{@resource.should(:ensure)}"
+    output = pkginfo "-qO", @resource[:name]
+    output.split("\n").each { |pkg_name| pkgdelete([pkg_name]) }
   end
 end
