nwn-lib 0.4.6 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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