nwn-lib 0.4.6 → 0.4.7

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -203,3 +203,38 @@ Bernhard Stoeckner <elven@swordcoast.net> (26):
203
203
  to the newly-added specs.
204
204
 
205
205
  Please browse the commit log for a full list of changes.
206
+
207
+ Bernhard Stoeckner <elven@swordcoast.net> (25):
208
+ bin/*: do not require rubygems explicitly
209
+ remove obsolete Gff::Helpers module
210
+ Erf: add support for ERF V1.1
211
+ env var NWN_LIB_DEBUG sends debug messages to stderr when set
212
+ add support for Microsoft Windows
213
+ R::ContentObject: do not fail on missing io, seek properly
214
+ R::ContentObject#filename: "unknown-N" instead of nil for unknown res_types
215
+ R::ContentObject: properly raise ENOENT on invalid io and file
216
+ G::Reader, Writer: work on io objects instead of strings
217
+ G::Writer: fix struct verify exception on reading substructs
218
+ specs: move all test stubs into _helper
219
+ Erf: improve broken locstr header handling
220
+ Erf: raise errors for invalid filenames on add, add_file
221
+ Erf: fix misaligned offsets in reslist
222
+ nwn-erf: fix parsing erf files where day_of_year = 0
223
+ nwn-erf: output non-errors to stdout instead of stderr
224
+ nwn-erf: fix packing up files
225
+ nwn-erf: add specs
226
+ Erf: reproduction spec checks for binary equality with sample
227
+ Gff: add specs for each_by_flat_path, to_gff
228
+ nwn-gff: fix backup strategy handler missing FileUtils
229
+ nwn-gff: add specs
230
+ rename spec/wellformed_gff.binary to .bic
231
+ nwn-dsl: add specs
232
+ 0.4.7-rel
233
+
234
+ A real bugfix-release, this takes care of a whole swad of fixes
235
+ in erf and gff handling, and also improves Windows compatibility.
236
+
237
+ Particularily, erf writing was kind of broken in the previous release,
238
+ putting wrong offsets for files created with nwn-erf.
239
+
240
+ Please browse the commit log for a full list of changes.
data/Rakefile CHANGED
@@ -9,7 +9,7 @@ include FileUtils
9
9
  # Configuration
10
10
  ##############################################################################
11
11
  NAME = "nwn-lib"
12
- VERS = "0.4.6"
12
+ VERS = "0.4.7"
13
13
  CLEAN.include ["**/.*.sw?", "pkg", ".config", "rdoc", "coverage"]
14
14
  RDOC_OPTS = ["--quiet", "--line-numbers", "--inline-source", '--title', \
15
15
  'nwn-lib: a ruby library for accessing NWN resource files', \
data/bin/nwn-erf CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/ruby
2
- require 'rubygems'
3
2
  require 'optparse'
4
3
  require 'nwn/all'
5
4
  require 'tempfile'
@@ -9,6 +8,7 @@ $verbose = false
9
8
  $type = 'ERF'
10
9
  $allow_duplicates = false
11
10
  $descriptions = {}
11
+ $version = "V1.0"
12
12
 
13
13
  # tar-compat mode: first argument is options if no dash is specified.
14
14
  ARGV[0] = "-" + ARGV[0] if ARGV.size > 0 && ARGV[0][0] != ?-
@@ -63,6 +63,13 @@ begin OptionParser.new do |o|
63
63
  $type = 'MOD'
64
64
  end
65
65
 
66
+ o.on "-0", "Create (only -c) V1.0 ERF, 16 byte resrefs. (NWN1, default)" do
67
+ $version = "V1.0"
68
+ end
69
+ o.on "-1", "Create (only -c) V1.0 ERF, 32 byte resrefs. (NWN2)." do
70
+ $version = "V1.1"
71
+ end
72
+
66
73
  o.separator " "
67
74
  o.separator "Hacks:"
68
75
 
@@ -91,15 +98,15 @@ def input filename = nil
91
98
  if $file.nil?
92
99
  yield $stdin
93
100
  else
94
- File.open(filename || $file, "r") {|f| yield f}
101
+ File.open(filename || $file, "rb") {|f| yield f}
95
102
  end
96
103
  end
97
104
 
98
- def output mode = "w", filename = nil
105
+ def output filename = nil
99
106
  if $file.nil?
100
107
  yield $stdout
101
108
  else
102
- File.open(filename || $file, mode) {|f| yield f}
109
+ File.open(filename || $file, "wb") {|f| yield f}
103
110
  end
104
111
  end
105
112
 
@@ -107,14 +114,14 @@ case $action
107
114
  when :t
108
115
  input {|f|
109
116
  erf = NWN::Erf::Erf.new(f)
110
- $stderr.puts "# %14s %16s %-10s %-4s %s" % %w{offset size date type filename} if $verbose
117
+ puts "# %14s %16s %-10s %-4s %s" % %w{offset size date type filename} if $verbose
111
118
  erf.content.each {|c|
112
119
  if !$verbose
113
- $stderr.puts "%s" % [c.filename]
120
+ puts "%s" % [c.filename]
114
121
  else
115
- $stderr.puts "%16d %16d %10s %4d %s" % [
122
+ puts "%16d %16d %10s %4d %s" % [
116
123
  c.offset, c.size,
117
- Date.ordinal(1900 + erf.year, erf.day_of_year).strftime("%Y-%m-%d"),
124
+ Date.ordinal(1900 + erf.year, 1 + erf.day_of_year).strftime("%Y-%m-%d"),
118
125
  c.res_type, c.filename
119
126
  ]
120
127
  end
@@ -125,8 +132,8 @@ case $action
125
132
  input {|f|
126
133
  erf = NWN::Erf::Erf.new(f)
127
134
  erf.content.each {|c|
128
- $stderr.puts "%s" % [c.filename] if $verbose
129
- File.open(c.filename, "w") {|ff|
135
+ puts "%s" % [c.filename] if $verbose
136
+ output(c.filename) {|ff|
130
137
  ff.write(c.get)
131
138
  }
132
139
  }
@@ -135,6 +142,7 @@ case $action
135
142
  when :c
136
143
  erf = NWN::Erf::Erf.new
137
144
  erf.file_type = $type if $type
145
+ erf.file_version = $version
138
146
 
139
147
  if $descriptions
140
148
  erf.localized_strings.merge! $descriptions
@@ -142,10 +150,10 @@ case $action
142
150
  end
143
151
 
144
152
  ARGV.each {|a|
145
- $stderr.puts File.basename(a) if $verbose
153
+ puts File.basename(a) if $verbose
146
154
  raise ArgumentError, "#{File.basename(a)} already present in erf." if
147
155
  !$allow_duplicates && erf.has?(a)
148
- erf.add a
156
+ erf.add_file a
149
157
  }
150
158
  output {|f| erf.write_to(f) }
151
159
 
@@ -163,7 +171,7 @@ case $action
163
171
  when :a
164
172
  raise ArgumentError, "#{File.basename(arg)} already present in erf." if
165
173
  !$allow_duplicates && erf.has?(arg)
166
- erf.add arg
174
+ erf.add_file arg
167
175
 
168
176
  when :r
169
177
  erf.content.reject! {|con|
@@ -173,7 +181,7 @@ case $action
173
181
  }
174
182
 
175
183
  tempfile = Tempfile.new("nwn-erf", File.dirname($file || "."))
176
- output("w", tempfile.path) do |f|
184
+ output(tempfile.path) do |f|
177
185
  erf.write_to(f)
178
186
  end
179
187
 
data/bin/nwn-gff CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/ruby
2
- require 'rubygems'
3
2
  require 'optparse'
4
3
  require 'nwn/all'
4
+ require 'fileutils'
5
5
 
6
6
  Thread.abort_on_exception = true
7
7
 
@@ -116,7 +116,7 @@ elsif :in == $options[:outformat]
116
116
  end
117
117
 
118
118
  vputs "Reading: #{$options[:infile]}"
119
- data_in = $options[:infile] == '-' ? $stdin.read : IO.read($options[:infile])
119
+ data_in = $options[:infile] == '-' ? $stdin.read : File.open($options[:infile], "rb")
120
120
  data_in = NWN::Gff.read(data_in, $options[:informat])
121
121
 
122
122
  # verify that we read a GFF struct
@@ -193,6 +193,6 @@ if $options[:backup] && FileTest.exists?($options[:outfile])
193
193
  FileUtils.cp($options[:outfile], bdest) if bdest
194
194
  end
195
195
 
196
- write_to = $options[:outfile] == '-' ? $stdout : File.open($options[:outfile], 'w')
196
+ write_to = $options[:outfile] == '-' ? $stdout : File.open($options[:outfile], 'wb')
197
197
 
198
198
  NWN::Gff.write(write_to, $options[:outformat], data_in)
data/bin/nwn-irb CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/ruby
2
- require 'rubygems'
3
2
  require 'optparse'
4
3
  require 'nwn/all'
5
4
  require 'irb'
@@ -19,7 +18,7 @@ def save destination = nil
19
18
 
20
19
  File.expand_path(destination ||= $file)
21
20
  $stderr.puts "Saving to `#{destination}' .."
22
- File.open(destination, "w") {|d|
21
+ File.open(destination, "wb") {|d|
23
22
  NWN::Gff.write(d, NWN::Gff.guess_file_format(destination), $gff)
24
23
  }
25
24
  $stderr.puts "saved."
data/lib/nwn/all.rb CHANGED
@@ -6,5 +6,4 @@ require 'nwn/erf'
6
6
  require 'nwn/yaml'
7
7
  require 'nwn/kivinen'
8
8
  require 'nwn/scripting'
9
- require 'nwn/helpers'
10
9
  require 'nwn/settings'
data/lib/nwn/erf.rb CHANGED
@@ -28,8 +28,34 @@ module NWN
28
28
  read_from io if io
29
29
  end
30
30
 
31
+ def add_file filename, io = nil
32
+ fnlen = filename_length @file_version
33
+ raise ArgumentError, "Invalid filename: #{filename.inspect}" if
34
+ filename.size == 0 || filename.size > (fnlen + 4)
35
+ super(filename, io)
36
+ end
37
+
38
+ def add o
39
+ fnlen = filename_length @file_version
40
+ raise ArgumentError, "Invalid filename: #{o.filename.inspect}" if
41
+ o.resref.size == 0 || o.resref.size > fnlen
42
+ super(o)
43
+ end
44
+
31
45
  private
32
46
 
47
+ def filename_length version
48
+ case version
49
+ when "V1.0"
50
+ 16
51
+ when "V1.1"
52
+ 32
53
+ else
54
+ raise IOError, "Invalid ERF version: #{version}"
55
+ end
56
+ end
57
+
58
+
33
59
  def read_from io
34
60
  header = @io.read(160)
35
61
  raise IOError, "Cannot read header: Not a erf file?" unless
@@ -46,34 +72,44 @@ module NWN
46
72
  raise IOError, "Cannot read erf stream: invalid type #{@file_type.inspect}" unless
47
73
  NWN::Erf::ValidTypes.index(@file_type)
48
74
 
49
- if @file_version == "V1.0"
50
- @filename_length = 16
51
- else
52
- raise IOError, "Invalid erf version: #{@file_version}"
53
- end
75
+ fnlen = filename_length @file_version
54
76
 
55
77
  @io.seek(offset_to_locstr)
56
78
  locstr = @io.read(locstr_size)
57
79
 
58
80
  for lstr in 0...locstr_count do
59
- raise IOError, "locstr table does not contain enough entries or locstr_size is too small" if
60
- locstr.nil? || locstr.size < 8
81
+ if locstr.nil? || locstr.size == 0
82
+ NWN.log_debug "locstr table: not enough entries (expected: #{locstr_count}, got: #{lstr})"
83
+ break
84
+ end
85
+
86
+ if locstr.size < 8
87
+ NWN.log_debug "locstr table: not enough entries (expected: #{locstr_count}, got: #{lstr})" +
88
+ " partial data: #{locstr.inspect}"
89
+ break
90
+ end
61
91
 
62
92
  lid, strsz = locstr.unpack("V V")
93
+ if strsz > locstr.size - 8
94
+ NWN.log_debug "locstr table: given strsz is bigger than available data, truncating"
95
+ strsz = locstr.size - 8
96
+ end
63
97
  str = locstr.unpack("x8 a#{strsz}")[0]
64
- raise IOError,
65
- "Expected locstr size does not match actual string size (want: #{strsz}, got #{str.size} of #{str.inspect})" if
66
- strsz != str.size
98
+
99
+ # This just means that the given locstr size was encoded wrongly -
100
+ # the old erf.exe is known to do that.
101
+ NWN.log_debug "Expected locstr size does not match actual " +
102
+ "string size (want: #{strsz}, got #{str.size} of #{str.inspect})" if strsz != str.size
67
103
 
68
104
  @localized_strings[lid] = str
69
105
  locstr = locstr[8 + str.size .. -1]
70
106
  end
71
107
 
72
- keylist_entry_size = @filename_length + 4 + 2 + 2
108
+ keylist_entry_size = fnlen + 4 + 2 + 2
73
109
  @io.seek(offset_to_keys)
74
110
  keylist = @io.read(keylist_entry_size * entry_count)
75
111
  raise IOError, "keylist too short" if keylist.size != keylist_entry_size * entry_count
76
- keylist = keylist.unpack("A16 V v v" * entry_count)
112
+ keylist = keylist.unpack("A#{fnlen} V v v" * entry_count)
77
113
  keylist.each_slice(4) {|resref, res_id, res_type, unused|
78
114
  @content << NWN::Resources::ContentObject.new(resref, res_type, @io)
79
115
  }
@@ -95,9 +131,21 @@ module NWN
95
131
 
96
132
  # Writes this Erf to a io stream.
97
133
  def write_to io
134
+ fnlen = filename_length @file_version
135
+
98
136
  locstr = @localized_strings.map {|x| [x[0], x[1].size, x[1]].pack("V V a*") }.join("")
99
- keylist = @content.map {|c| [c.resref, @content.index(c), c.res_type, 0].pack("a16 V v v") }.join("")
100
- reslist = @content.map {|c| [c.offset, c.size].pack("V V") }.join("")
137
+ keylist = @content.map {|c|
138
+ NWN.log_debug "truncating filename #{c.resref}, longer than #{fnlen}" if c.resref.size > fnlen
139
+ [c.resref, @content.index(c), c.res_type, 0].pack("a#{fnlen} V v v")
140
+ }.join("")
141
+
142
+ pre_offset = 160 + locstr.size + keylist.size + 8 * @content.size
143
+
144
+ reslist = @content.map {|c|
145
+ offset = pre_offset +
146
+ @content[0, @content.index(c)].inject(0) {|sum,x| sum + x.size }
147
+ [offset, c.size].pack("V V")
148
+ }.join("")
101
149
 
102
150
  offset_to_locstr = 160
103
151
  offset_to_keylist = offset_to_locstr + locstr.size
data/lib/nwn/gff.rb CHANGED
@@ -94,7 +94,7 @@ module NWN
94
94
  def self.write(io, format, data)
95
95
  case format
96
96
  when :gff
97
- io.print NWN::Gff::Writer.dump(data)
97
+ NWN::Gff::Writer.dump(data, io)
98
98
  when :yaml
99
99
  io.puts data.to_yaml
100
100
  when :marshal
@@ -4,22 +4,26 @@ class NWN::Gff::Reader
4
4
 
5
5
  attr_reader :root_struct
6
6
 
7
- # Create a new Reader with the given +bytes+ and immediately parse it.
8
- # This is not needed usually; use Reader.read instead.
9
- def initialize bytes
10
- @bytes = bytes
7
+ private_class_method :new
11
8
 
12
- read_all
9
+ # Create a new Reader with the given +io+ and immediately parse it.
10
+ def self.read io
11
+ t = new(io)
12
+ t.root_struct
13
13
  end
14
14
 
15
- # Reads +bytes+ as gff data and returns a NWN::Gff:Gff object.
16
- def self.read bytes
17
- self.new(bytes).root_struct
15
+ def initialize io #:nodoc:
16
+ @io = io
17
+ read_all
18
18
  end
19
19
 
20
20
  private
21
21
 
22
22
  def read_all
23
+ header = @io.read(160)
24
+ raise IOError, "Cannot read header" unless header &&
25
+ header.size == 160
26
+
23
27
  type, version,
24
28
  struct_offset, struct_count,
25
29
  field_offset, field_count,
@@ -27,9 +31,9 @@ class NWN::Gff::Reader
27
31
  field_data_offset, field_data_count,
28
32
  field_indices_offset, field_indices_count,
29
33
  list_indices_offset, list_indices_count =
30
- @bytes.unpack("a4a4 VV VV VV VV VV VV")
34
+ header.unpack("a4a4 VV VV VV VV VV VV")
31
35
 
32
- raise GffError, "Unknown version #{@version}; not a gff?" unless
36
+ raise GffError, "Unknown version #{version}; not a gff?" unless
33
37
  version == "V3.2"
34
38
 
35
39
  raise GffError, "struct offset at wrong place, not a gff?" unless
@@ -39,12 +43,35 @@ class NWN::Gff::Reader
39
43
  field_len = field_count * 16
40
44
  label_len = label_count * 16
41
45
 
42
- @structs = @bytes[struct_offset, struct_len].unpack("V*")
43
- @fields = @bytes[field_offset, field_len].unpack("V*")
44
- @labels = @bytes[label_offset, label_len].unpack("A16" * label_count)
45
- @field_data = @bytes[field_data_offset, field_data_count]
46
- @field_indices = @bytes[field_indices_offset, field_indices_count].unpack("V*")
47
- @list_indices = @bytes[list_indices_offset, list_indices_count].unpack("V*")
46
+ @io.seek(struct_offset)
47
+ @structs = @io.read(struct_len)
48
+ raise IOError, "cannot read structs" unless @structs && @structs.size == struct_len
49
+ @structs = @structs.unpack("V*")
50
+
51
+ @io.seek(field_offset)
52
+ @fields = @io.read(field_len)
53
+ raise IOError, "cannot read fields" unless @fields && @fields.size == field_len
54
+ @fields = @fields.unpack("V*")
55
+
56
+ @io.seek(label_offset)
57
+ @labels = @io.read(label_len)
58
+ raise IOError, "cannot read labels" unless @labels && @labels.size == label_len
59
+ @labels = @labels.unpack("A16" * label_count)
60
+
61
+ @io.seek(field_data_offset)
62
+ @field_data = @io.read(field_data_count)
63
+ raise IOError, "cannot read field_data" unless @field_data && @field_data.size == field_data_count
64
+
65
+ @io.seek(field_indices_offset)
66
+ @field_indices = @io.read(field_indices_count)
67
+ raise IOError, "cannot read field_indices" unless @field_indices && @field_indices.size == field_indices_count
68
+ @field_indices = @field_indices.unpack("V*")
69
+
70
+ @io.seek(list_indices_offset)
71
+ @list_indices = @io.read(list_indices_count)
72
+ raise IOError, "cannot read list_indices" unless @list_indices && @list_indices.size == list_indices_count
73
+ @list_indices = @list_indices.unpack("V*")
74
+
48
75
  @root_struct = read_struct 0, type.strip, version
49
76
  end
50
77
 
@@ -1,10 +1,25 @@
1
1
  class NWN::Gff::Writer
2
- include NWN
3
2
  include NWN::Gff
4
3
 
4
+ private_class_method :new
5
+
5
6
  attr_reader :bytes
6
7
 
7
- def initialize(gff, data_type = nil)
8
+ # Takes a NWN::Gff::Gff object and dumps it to +io+,
9
+ # including the header.
10
+ # If +io+ is nil, return the raw bytes, otherwise
11
+ # the number of bytes written.
12
+ def self.dump(gff, io = nil, data_type = nil)
13
+ ret = new(gff, data_type).bytes
14
+ if io
15
+ io.write(ret)
16
+ ret.size
17
+ else
18
+ ret
19
+ end
20
+ end
21
+
22
+ def initialize(gff, data_type = nil) #:nodoc:
8
23
  @gff = gff
9
24
  @data_type = data_type
10
25
 
@@ -18,12 +33,6 @@ class NWN::Gff::Writer
18
33
  write_all
19
34
  end
20
35
 
21
- # Takes a NWN::Gff::Gff object and dumps it as raw bytes,
22
- # including the header.
23
- def self.dump(gff, data_type = nil)
24
- self.new(gff, data_type).bytes
25
- end
26
-
27
36
  private
28
37
 
29
38
  def get_label_id_for_label str
@@ -127,7 +136,7 @@ private
127
136
 
128
137
  when :struct
129
138
  raise GffError, "type = struct, but value not a hash" unless
130
- v.field_value.is_a?(Gff::Struct)
139
+ v.field_value.is_a?(Struct)
131
140
 
132
141
  fields_of_this_struct << add_data_field(v.field_type, k, write_struct(v.field_value))
133
142