smc-get 0.2.0.beta1 → 0.3.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,80 +1,186 @@
1
- #Encoding: UTF-8
2
- ################################################################################
3
- # This file is part of smc-get.
4
- # Copyright (C) 2010-2011 Entertaining Software, Inc.
5
- # Copyright (C) 2011 Marvin Gülker
6
- #
7
- # This program is free software: you can redistribute it and/or modify
8
- # it under the terms of the GNU General Public License as published by
9
- # the Free Software Foundation, either version 3 of the License, or
10
- # (at your option) any later version.
11
- #
12
- # This program is distributed in the hope that it will be useful,
13
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- # GNU General Public License for more details.
16
- #
17
- # You should have received a copy of the GNU General Public License
18
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
- ################################################################################
20
-
21
- module SmcGet
22
-
23
- #This class allows for easy compression and decompression of .smcpak
24
- #files. Please note that during this process no validations are performed,
25
- #i.e. you have to ensure that the directory you want to compress is in
26
- #smc package layout and the .smcpak files you want to decompress are
27
- #really smc packages.
28
- class PackageArchive
29
-
30
- #The location of this archive.
31
- attr_reader :path
32
-
33
- #Compresses all files in +directory+ into a TAR.XZ file with the
34
- #extension ".smcpak".
35
- #Returns an object of this class.
36
- # smcpak = PackageArchive.compress("mypackage")
37
- # puts smcpak.path #=> mypackage.smcpak
38
- def self.compress(directory, goal_file)
39
- directory = Pathname.new(directory).expand_path
40
- tar_file = Pathname.new("#{goal_file}.tar").expand_path
41
- xz_file = Pathname.new(goal_file).expand_path
42
-
43
- Dir.chdir(directory.parent) do
44
- tar_file.open("wb") do |file|
45
- Archive::Tar::Minitar.pack(directory.relative_path_from(Pathname.new(".").expand_path).to_s, file)
46
- end
47
- end
48
- XZ.compress_file(tar_file, xz_file)
49
-
50
- tar_file.delete #We don’t need it anymore
51
-
52
- new(xz_file)
53
- end
54
-
55
- #Creates a new PackageArchive from an existing .smcpak file.
56
- def initialize(archive)
57
- @path = Pathname.new(archive)
58
- end
59
-
60
- #Decompresses this archive into +directory+, creating a subdirectory
61
- #named after the archive without the extension, and returns the path
62
- #to that subdirectory (a Pathname object).
63
- # smcpak = PackageArchive.new("mypackage.smcpak")
64
- # puts smcpak.decompress #=> #<Pathname:mypackage>
65
- def decompress(directory)
66
- directory = Pathname.new(directory)
67
- tar_file = directory + @path.basename.to_s.sub(/\.smcpak$/, ".tar")
68
- dest_dir = directory + tar_file.basename.to_s.sub(/\.tar$/, "")
69
-
70
- XZ.decompress_file(@path.to_s, tar_file.to_s)
71
- Archive::Tar::Minitar.unpack(tar_file.to_s, dest_dir.to_s)
72
-
73
- tar_file.delete #We don’ŧ need it anymore
74
-
75
- dest_dir
76
- end
77
-
78
- end
79
-
80
- end
1
+ # -*- coding: utf-8 -*-
2
+ ################################################################################
3
+ # This file is part of smc-get.
4
+ # Copyright (C) 2010-2011 Entertaining Software, Inc.
5
+ # Copyright (C) 2011 Marvin Gülker
6
+ #
7
+ # This program is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # This program is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
+ ################################################################################
20
+
21
+ module SmcGet
22
+
23
+ #This class allows for easy compression and decompression of .smcpak
24
+ #files. Please note that during this process no validations are performed,
25
+ #i.e. you have to ensure that the directory you want to compress is in
26
+ #smc package layout and the .smcpak files you want to decompress are
27
+ #really smc packages.
28
+ class PackageArchive
29
+
30
+ #Number of bytes to read from a file at a time when it’s being
31
+ #compressed.
32
+ CHUNK_SIZE = 4096
33
+
34
+ #The location of this archive. A Pathname object.
35
+ attr_reader :path
36
+
37
+ class << self
38
+
39
+ #Compresses all files in +directory+ into a TAR.XZ file with the
40
+ #extension ".smcpak".
41
+ #==Parameters
42
+ #[directory] The directory to compress. This will be the toplevel
43
+ # directory in the resulting TAR file.
44
+ #[goal_file] Full path to where to place the TAR file.
45
+ #==Return value
46
+ #Returns an object of this class.
47
+ #==Examples
48
+ # smcpak = PackageArchive.compress("/home/freak/packages/mypackage", "mypackage.smcpak")
49
+ # puts smcpak.path #=> mypackage.smcpak
50
+ #
51
+ # smcpak = PackageArchive.compress("mypackage", "/home/freak/compressed_packages/mypackage.smcpak")
52
+ # puts smcpak.path #=> /home/freak/compressed_packages/mypackage.smcpak
53
+ def compress(directory, goal_file)
54
+ directory = Pathname.new(directory).expand_path
55
+ tar_file = Pathname.new("#{goal_file}.tar").expand_path
56
+ xz_file = Pathname.new(goal_file).expand_path
57
+
58
+ tar_file.open("wb"){|file| compress_dir(directory, file)}
59
+ XZ.compress_file(tar_file, xz_file)
60
+
61
+ tar_file.delete #We don’t need it anymore
62
+
63
+ new(xz_file)
64
+ end
65
+
66
+ private
67
+
68
+ #This method creates a TAR archive inside the given +io+ by
69
+ #recursively packaging all files found in +path+ into it.
70
+ #
71
+ #Note that this method isn’t quite the same as <tt>Minitar.pack</tt>,
72
+ #because there is a big difference in how absolute paths are handled.
73
+ #Consider the following call:
74
+ #
75
+ # Minitar.pack("/home/freak/foo", file)
76
+ #
77
+ #Inside the TAR you’ll get this:
78
+ #
79
+ # /
80
+ # home/
81
+ # freak/
82
+ # foo/
83
+ # 1.txt
84
+ # 2.rb
85
+ #
86
+ #Surely not what you wanted, plus a leading / that can be misinterpreted
87
+ #by some archiving programs (e.g. XFCE’s XArchiver). Now compare that
88
+ #to what this method does:
89
+ #
90
+ # compress_dir("/home/freak/foo", file)
91
+ #
92
+ #Result in the TAR:
93
+ #
94
+ # foo/
95
+ # 1.txt
96
+ # 2.txt
97
+ #
98
+ #It strippes off the unwanted prefix and just places the actual contents of
99
+ #the last element in +path+ (and it’s subdirectories) inside the TAR
100
+ #archive. For a +path+ relative to the current working directory however
101
+ #this method should behave the same as the usual <tt>Minitar.pack</tt>.
102
+ #
103
+ #Extra bonus: This method is threadsafe, as it doesn’t use +chdir+ to
104
+ #change the directory representation in the TAR file. It uses Minitar’s
105
+ #lowlevel methods instead.
106
+ def compress_dir(path, io)
107
+ raise(ArgumentError, "#{path} is not a directory!") unless path.directory?
108
+
109
+ Archive::Tar::Minitar::Output.open(io) do |output|
110
+ tar = output.tar #Get the real Writer object
111
+
112
+ path.find do |entry|
113
+ #This is the path as it will show up inside the tar
114
+ relative_path = entry.relative_path_from(path.parent) #parent b/c first entry must be the toplevel dirname, not "."
115
+
116
+ #Copy permissions from the original file for the tar’ed file
117
+ stat = entry.stat
118
+ stats = {
119
+ :mode => stat.mode,
120
+ :mtime => stat.mtime,
121
+ :size => stat.size,
122
+ :uid => stat.uid, #Should be nil on Windows
123
+ :gid => stat.gid #Should be nil on Windows
124
+ }
125
+
126
+ if entry.directory?
127
+ tar.mkdir(relative_path.to_s, stats)
128
+ elsif entry.file?
129
+ tar.add_file_simple(relative_path.to_s, stats) do |stream|
130
+ File.open(entry, "rb") do |file|
131
+ chunk = nil
132
+ stream.write(chunk) while chunk = file.read(CHUNK_SIZE)
133
+ end
134
+ end
135
+ else
136
+ raise(Errors::CompressionError.new("Unsupported file type for #{entry}!", entry))
137
+ end #if
138
+ end #find
139
+ end #Output.new
140
+ end #compress_dir
141
+
142
+ end
143
+
144
+ #Creates a new PackageArchive from an existing .smcpak file.
145
+ #==Parameter
146
+ #[archive] The path to the file.
147
+ #==Return value
148
+ #A new instance of this class.
149
+ #==Example
150
+ # archive = PackageArchive.new("/home/freak/compressed_packages/mypackage.smcpak")
151
+ def initialize(archive)
152
+ @path = Pathname.new(archive)
153
+ end
154
+
155
+ #Decompresses this archive, creating a subdirectory
156
+ #named after the archive without the extension.
157
+ #==Parameters
158
+ #[directory] Where to extract the archive to. Note a subdirectory
159
+ # will be created below this path.
160
+ #==Return value
161
+ #The path to the subdirectory, a Pathname object.
162
+ #==Example
163
+ # smcpak = PackageArchive.new("mypackage.smcpak")
164
+ # puts smcpak.decompress #=> #<Pathname:mypackage>
165
+ def decompress(directory)
166
+ directory = Pathname.new(directory)
167
+ tar_file = directory + @path.basename.to_s.sub(/\.smcpak$/, ".tar")
168
+ dest_dir = directory + tar_file.basename.to_s.sub(/\.tar$/, "")
169
+
170
+ XZ.decompress_file(@path.to_s, tar_file.to_s)
171
+ Archive::Tar::Minitar.unpack(tar_file.to_s, dest_dir.to_s)
172
+
173
+ tar_file.delete #We don’t need it anymore
174
+
175
+ dest_dir
176
+ end
177
+
178
+ #Human-readable description of form:
179
+ # #<SmcGet::PackageArchive <path>>
180
+ def inspect
181
+ "#<#{self.class} #{@path.expand_path}>"
182
+ end
183
+
184
+ end
185
+
186
+ end
@@ -1,253 +1,257 @@
1
- #Encoding: UTF-8
2
- ################################################################################
3
- # This file is part of smc-get.
4
- # Copyright (C) 2010-2011 Entertaining Software, Inc.
5
- # Copyright (C) 2011 Marvin Gülker
6
- #
7
- # This program is free software: you can redistribute it and/or modify
8
- # it under the terms of the GNU General Public License as published by
9
- # the Free Software Foundation, either version 3 of the License, or
10
- # (at your option) any later version.
11
- #
12
- # This program is distributed in the hope that it will be useful,
13
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- # GNU General Public License for more details.
16
- #
17
- # You should have received a copy of the GNU General Public License
18
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
- ################################################################################
20
-
21
- module SmcGet
22
-
23
- #A PackageSpecification object is a mostly informational object
24
- #directly related to Package objects. That is, each and every
25
- #Package instance has a Package#spec method that will retrieve
26
- #the PackageSpecification, an instance of this class, for you:
27
- #
28
- # puts pkg.spec.title #=> Cool package
29
- #
30
- #Instances of this class can be understood as the parsed content of
31
- #a package’s specification file. It has getter and setter methods
32
- #reflecting the keys that are allowed in the specification (see
33
- #the smcpak.rdoc file), but unless you want to build smc packages,
34
- #there’s no need for you to use the setter methods.
35
- class PackageSpecification
36
-
37
- #The keys listed here must be mentioned inside a package spec,
38
- #otherwise the package is considered broken.
39
- SPEC_MANDATORY_KEYS = [:title, :last_update, :authors, :difficulty, :description, :checksums].freeze
40
-
41
- ##
42
- # :attr_accessor: title
43
- #The package’s title.
44
-
45
- ##
46
- # :attr_accessor: last_update
47
- #The time (an instance of class Time) indicating when the package
48
- #was last updated.
49
-
50
- ##
51
- # :attr_accessor: authors
52
- #The authors of this package. An array.
53
-
54
- ##
55
- # :attr_accessor: difficulty
56
- #The difficulty of this package as a string.
57
-
58
- ##
59
- # :attr_accessor: description
60
- #The description of this package.
61
-
62
- ##
63
- # :attr_accessor: install_message
64
- #A message to display during installation of this package or nil if
65
- #no message shall be displayed.
66
-
67
- ##
68
- # :attr_accessor: remove_message
69
- #A message to display during removing of this package or nil if no
70
- #message shall be displayed.
71
-
72
- ##
73
- # :attr_accessor: dependecies
74
- #An array of package names this package depends on, i.e. packages that
75
- #need to be installed before this one can be installed. The array is
76
- #empty if no dependecies exist.
77
-
78
- ##
79
- # :attr_accessor: levels
80
- #An array of level file names (strings).
81
-
82
- ##
83
- # :attr_accessor: music
84
- #An array of music file names (strings).
85
-
86
- ##
87
- # :attr_accessor: sounds
88
- #An array of sound file names (strings).
89
-
90
- ##
91
- # :attr_accessor: graphics
92
- #An array of graphic file names (strings).
93
-
94
- ##
95
- # :attr_accessor: worlds
96
- #An array of graphic file names (strings).
97
-
98
- ##
99
- # :attr_accessor: checksums
100
- #A hash that maps each filename in this package to it’s SHA1 checksum.
101
-
102
- #The name of the package this specification is used in, without any
103
- #file extension.
104
- attr_reader :name
105
-
106
- ##
107
- # :attr_reader: compressed_file_name
108
- #The name of the compressed file this specification should belong to.
109
- #The same as name, but the extension .smcpak was appended.
110
-
111
- ##
112
- # :attr_reader: spec_file_name
113
- #The name of the specification file. The same as name, but
114
- #the extension .yml was appended.
115
-
116
- #Creates a PackageSpecification by directly reading a complete
117
- #spec from a YAML file.
118
- #==Parameter
119
- #[path] The path to the file to read.
120
- #==Return value
121
- #An instance of this class.
122
- #==Raises
123
- #[InvalidSpecification] +path+ was not found or was malformatted.
124
- #==Example
125
- # path = remote_repo.fetch_spec("cool_pkg.yml")
126
- # spec = SmcGet::PackageSpecification.from_file(path)
127
- # puts spec.title #=> "Cool package"
128
- #==Remarks
129
- #This method may be useful if you don’t need a full-blown
130
- #Package object and just want to deal with it’s most important
131
- #attributes.
132
- def self.from_file(path)
133
- info = nil
134
- begin
135
- info = YAML.load_file(path.to_s)
136
- rescue Errno::ENOENT => e
137
- raise(Errors::InvalidSpecification, "File '#{path}' doesn't exist!")
138
- rescue => e
139
- raise(Errors::InvalidSpecification, "Invalid YAML: #{e.message}")
140
- end
141
-
142
- spec = new(File.basename(path).sub(/\.yml$/, ""))
143
- info.each_pair do |key, value|
144
- spec.send(:"#{key}=", value)
145
- end
146
- #TODO: Convert the strings in :checksums to strings, except the
147
- #filenames, those should be strings. Anyone???
148
-
149
- raise(Errors::InvalidSpecification, spec.validate.first) unless spec.valid?
150
-
151
- spec
152
- end
153
-
154
- #Returns the matching package name from the package specification’s name
155
- #by replacing the .yml extension with .smcpak.
156
- #==Return value
157
- #A string ending in ".smcpak".
158
- #==Example
159
- # p SmcGet::PackageSpecification.spec2pkg("cool_pkg.yml") #=> "cool_pkg.smcpak")
160
- def self.spec2pkg(spec_file_name) # :nodoc:
161
- spec_file_name.to_s.sub(/\.yml$/, ".smcpak")
162
- end
163
-
164
- #Returns the matching specification file name from the package’s name
165
- #by replacing the .smcpak extension with .yml.
166
- #==Return value
167
- #A string ending in ".yml"
168
- #==Example
169
- # p SmcGet::PackageSpecification.pkg2spec("cool_pkg.smcpak") #=> "cool_pkg.yml"
170
- def self.pkg2spec(package_file_name) # :nodoc:
171
- package_file_name.to_s.sub(/\.smcpak$/, ".yml")
172
- end
173
-
174
- def initialize(pkg_name)
175
- @info = {:dependencies => [], :levels => [], :music => [], :sounds => [], :graphics => [], :worlds => []}
176
- @name = pkg_name
177
- end
178
-
179
- #See attribute.
180
- def compressed_file_name # :nodoc:
181
- "#@name.smcpak"
182
- end
183
-
184
- #See attribute.
185
- def spec_file_name # :nodoc:
186
- "#@name.yml"
187
- end
188
-
189
- [:title, :last_update, :authors, :difficulty, :description, :install_message, :remove_message, :dependencies, :levels, :music, :sounds, :graphics, :worlds, :checksums].each do |sym|
190
- define_method(sym){@info[sym]}
191
- define_method(:"#{sym}="){|val| @info[sym] = val}
192
- end
193
-
194
- def [](sym)
195
- if respond_to?(sym)
196
- send(sym)
197
- else
198
- raise(IndexError, "No such specification key: #{sym}!")
199
- end
200
- end
201
-
202
- def valid?
203
- validate.empty?
204
- end
205
-
206
- def validate
207
- errors = []
208
-
209
- SPEC_MANDATORY_KEYS.each do |sym|
210
- errors << "Mandatory key #{sym} is missing!" unless @info.has_key?(sym)
211
- end
212
-
213
- errors
214
- end
215
-
216
- #Compares two specifications. They are considered equal if all their
217
- #attributes (levels, difficulty, etc.) are equal.
218
- def ==(other)
219
- return false unless other.respond_to? :info
220
- @info == other.info
221
- end
222
-
223
- #Saves the package specification in YAML format into a file.
224
- #==Parameter
225
- #[directory] The directory where to save the file to. The filename is automatically
226
- # detected from the attributes set for the specification.
227
- #==Raises
228
- #[InvalidSpecification] The specification was not valid, i.e. contained incorrect
229
- # or missing values.
230
- #==Example
231
- # p spec.name #=> "cool_pkg"
232
- # spec.save(".")
233
- # p File.file?("cool_pkg.yml") #=> true
234
- def save(directory)
235
- raise(Errors::InvalidSpecification, validate.first) unless valid?
236
-
237
- path = Pathname.new(directory) + "#{@name}.yml"
238
- #Turn the spec keys for serialization into strings
239
- hsh = {}
240
- @info.each_pair{|k, v| hsh[k.to_s] = v}
241
- path.open("w"){|f| YAML.dump(hsh, f)}
242
- end
243
-
244
- protected
245
-
246
- #Returns the complete internal information hash.
247
- def info
248
- @info
249
- end
250
-
251
- end
252
-
253
- end
1
+ #Encoding: UTF-8
2
+ ################################################################################
3
+ # This file is part of smc-get.
4
+ # Copyright (C) 2010-2011 Entertaining Software, Inc.
5
+ # Copyright (C) 2011 Marvin Gülker
6
+ #
7
+ # This program is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # This program is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
+ ################################################################################
20
+
21
+ module SmcGet
22
+
23
+ #A PackageSpecification object is a mostly informational object
24
+ #directly related to Package objects. That is, each and every
25
+ #Package instance has a Package#spec method that will retrieve
26
+ #the PackageSpecification, an instance of this class, for you:
27
+ #
28
+ # puts pkg.spec.title #=> Cool package
29
+ #
30
+ #Instances of this class can be understood as the parsed content of
31
+ #a package’s specification file. It has getter and setter methods
32
+ #reflecting the keys that are allowed in the specification (see
33
+ #the smcpak.rdoc file), but unless you want to build smc packages,
34
+ #there’s no need for you to use the setter methods.
35
+ class PackageSpecification
36
+
37
+ #The keys listed here must be mentioned inside a package spec,
38
+ #otherwise the package is considered broken.
39
+ SPEC_MANDATORY_KEYS = [:title, :last_update, :authors, :difficulty, :description, :checksums].freeze
40
+
41
+ ##
42
+ # :attr_accessor: title
43
+ #The package’s title.
44
+
45
+ ##
46
+ # :attr_accessor: last_update
47
+ #The time (an instance of class Time) indicating when the package
48
+ #was last updated.
49
+
50
+ ##
51
+ # :attr_accessor: authors
52
+ #The authors of this package. An array.
53
+
54
+ ##
55
+ # :attr_accessor: difficulty
56
+ #The difficulty of this package as a string.
57
+
58
+ ##
59
+ # :attr_accessor: description
60
+ #The description of this package.
61
+
62
+ ##
63
+ # :attr_accessor: install_message
64
+ #A message to display during installation of this package or nil if
65
+ #no message shall be displayed.
66
+
67
+ ##
68
+ # :attr_accessor: remove_message
69
+ #A message to display during removing of this package or nil if no
70
+ #message shall be displayed.
71
+
72
+ ##
73
+ # :attr_accessor: dependecies
74
+ #An array of package names this package depends on, i.e. packages that
75
+ #need to be installed before this one can be installed. The array is
76
+ #empty if no dependecies exist.
77
+
78
+ ##
79
+ # :attr_accessor: levels
80
+ #An array of level file names (strings).
81
+
82
+ ##
83
+ # :attr_accessor: music
84
+ #An array of music file names (strings).
85
+
86
+ ##
87
+ # :attr_accessor: sounds
88
+ #An array of sound file names (strings).
89
+
90
+ ##
91
+ # :attr_accessor: graphics
92
+ #An array of graphic file names (strings).
93
+
94
+ ##
95
+ # :attr_accessor: worlds
96
+ #An array of graphic file names (strings).
97
+
98
+ ##
99
+ # :attr_accessor: checksums
100
+ #A hash that maps each filename in this package to it’s SHA1 checksum.
101
+
102
+ #The name of the package this specification is used in, without any
103
+ #file extension.
104
+ attr_reader :name
105
+
106
+ ##
107
+ # :attr_reader: compressed_file_name
108
+ #The name of the compressed file this specification should belong to.
109
+ #The same as name, but the extension .smcpak was appended.
110
+
111
+ ##
112
+ # :attr_reader: spec_file_name
113
+ #The name of the specification file. The same as name, but
114
+ #the extension .yml was appended.
115
+
116
+ #Creates a PackageSpecification by directly reading a complete
117
+ #spec from a YAML file.
118
+ #==Parameter
119
+ #[path] The path to the file to read.
120
+ #==Return value
121
+ #An instance of this class.
122
+ #==Raises
123
+ #[InvalidSpecification] +path+ was not found or was malformatted.
124
+ #==Example
125
+ # path = remote_repo.fetch_spec("cool_pkg.yml")
126
+ # spec = SmcGet::PackageSpecification.from_file(path)
127
+ # puts spec.title #=> "Cool package"
128
+ #==Remarks
129
+ #This method may be useful if you don’t need a full-blown
130
+ #Package object and just want to deal with it’s most important
131
+ #attributes.
132
+ def self.from_file(path)
133
+ info = nil
134
+ begin
135
+ info = YAML.load_file(path.to_s)
136
+ rescue Errno::ENOENT => e
137
+ raise(Errors::InvalidSpecification, "File '#{path}' doesn't exist!")
138
+ rescue => e
139
+ raise(Errors::InvalidSpecification, "Invalid YAML: #{e.message}")
140
+ end
141
+
142
+ spec = new(File.basename(path).sub(/\.yml$/, ""))
143
+ info.each_pair do |key, value|
144
+ spec.send(:"#{key}=", value)
145
+ end
146
+ #TODO: Convert the strings in :checksums to strings, except the
147
+ #filenames, those should be strings. Anyone???
148
+
149
+ raise(Errors::InvalidSpecification, spec.validate.first) unless spec.valid?
150
+
151
+ spec
152
+ end
153
+
154
+ #Returns the matching package name from the package specification’s name
155
+ #by replacing the .yml extension with .smcpak.
156
+ #==Return value
157
+ #A string ending in ".smcpak".
158
+ #==Example
159
+ # p SmcGet::PackageSpecification.spec2pkg("cool_pkg.yml") #=> "cool_pkg.smcpak")
160
+ def self.spec2pkg(spec_file_name) # :nodoc:
161
+ spec_file_name.to_s.sub(/\.yml$/, ".smcpak")
162
+ end
163
+
164
+ #Returns the matching specification file name from the package’s name
165
+ #by replacing the .smcpak extension with .yml.
166
+ #==Return value
167
+ #A string ending in ".yml"
168
+ #==Example
169
+ # p SmcGet::PackageSpecification.pkg2spec("cool_pkg.smcpak") #=> "cool_pkg.yml"
170
+ def self.pkg2spec(package_file_name) # :nodoc:
171
+ package_file_name.to_s.sub(/\.smcpak$/, ".yml")
172
+ end
173
+
174
+ def initialize(pkg_name)
175
+ @info = {:dependencies => [], :levels => [], :music => [], :sounds => [], :graphics => [], :worlds => []}
176
+ @name = pkg_name
177
+ end
178
+
179
+ #See attribute.
180
+ def compressed_file_name # :nodoc:
181
+ "#@name.smcpak"
182
+ end
183
+
184
+ #See attribute.
185
+ def spec_file_name # :nodoc:
186
+ "#@name.yml"
187
+ end
188
+
189
+ [:title, :last_update, :authors, :difficulty, :description, :install_message, :remove_message, :dependencies, :levels, :music, :sounds, :graphics, :worlds, :checksums].each do |sym|
190
+ define_method(sym){@info[sym]}
191
+ define_method(:"#{sym}="){|val| @info[sym] = val}
192
+ end
193
+
194
+ def [](sym)
195
+ if respond_to?(sym)
196
+ send(sym)
197
+ else
198
+ raise(IndexError, "No such specification key: #{sym}!")
199
+ end
200
+ end
201
+
202
+ def valid?
203
+ validate.empty?
204
+ end
205
+
206
+ def validate
207
+ errors = []
208
+
209
+ SPEC_MANDATORY_KEYS.each do |sym|
210
+ errors << "Mandatory key #{sym} is missing!" unless @info.has_key?(sym)
211
+ end
212
+
213
+ [:levels, :graphics, :music, :sounds, :worlds].each do |sym|
214
+ errors << "Mandatory checksum key #{sym} is missing!" unless @info[:checksums].has_key?(sym.to_s)
215
+ end
216
+
217
+ errors
218
+ end
219
+
220
+ #Compares two specifications. They are considered equal if all their
221
+ #attributes (levels, difficulty, etc.) are equal.
222
+ def ==(other)
223
+ return false unless other.respond_to? :info
224
+ @info == other.info
225
+ end
226
+
227
+ #Saves the package specification in YAML format into a file.
228
+ #==Parameter
229
+ #[directory] The directory where to save the file to. The filename is automatically
230
+ # detected from the attributes set for the specification.
231
+ #==Raises
232
+ #[InvalidSpecification] The specification was not valid, i.e. contained incorrect
233
+ # or missing values.
234
+ #==Example
235
+ # p spec.name #=> "cool_pkg"
236
+ # spec.save(".")
237
+ # p File.file?("cool_pkg.yml") #=> true
238
+ def save(directory)
239
+ raise(Errors::InvalidSpecification, validate.first) unless valid?
240
+
241
+ path = Pathname.new(directory) + "#{@name}.yml"
242
+ #Turn the spec keys for serialization into strings
243
+ hsh = {}
244
+ @info.each_pair{|k, v| hsh[k.to_s] = v}
245
+ path.open("w"){|f| YAML.dump(hsh, f)}
246
+ end
247
+
248
+ protected
249
+
250
+ #Returns the complete internal information hash.
251
+ def info
252
+ @info
253
+ end
254
+
255
+ end
256
+
257
+ end