summaryrefslogtreecommitdiffhomepage
path: root/lib
diff options
context:
space:
mode:
authorRandy Morgan <[email protected]>2012-01-06 20:17:32 +0900
committerRandy Morgan <[email protected]>2012-01-06 20:17:32 +0900
commit20379c963661b8d2cfa79a194bb73bd620e5d1e7 (patch)
tree60a083728651c33b6da5f4f53ae89aca358a3f5d /lib
parent92cca55f4e23c7dedb71035a9711159773a51ed4 (diff)
downloadcaxlsx-20379c963661b8d2cfa79a194bb73bd620e5d1e7.tar.gz
caxlsx-20379c963661b8d2cfa79a194bb73bd620e5d1e7.zip
beginnings of password protected compound binary file using ECMA encryption
Diffstat (limited to 'lib')
-rw-r--r--lib/axlsx.rb3
-rw-r--r--lib/axlsx/package.rb6
-rw-r--r--lib/axlsx/util/cbf.rb252
-rw-r--r--lib/axlsx/util/ms_off_crypto.rb181
-rw-r--r--lib/axlsx/util/storage.rb146
5 files changed, 547 insertions, 41 deletions
diff --git a/lib/axlsx.rb b/lib/axlsx.rb
index 2f347d49..d60a4aa8 100644
--- a/lib/axlsx.rb
+++ b/lib/axlsx.rb
@@ -6,7 +6,8 @@ require 'axlsx/version.rb'
require 'axlsx/util/simple_typed_list.rb'
require 'axlsx/util/constants.rb'
require 'axlsx/util/validators.rb'
-
+require 'axlsx/util/storage.rb'
+require 'axlsx/util/cbf.rb'
require 'axlsx/util/ms_off_crypto.rb'
diff --git a/lib/axlsx/package.rb b/lib/axlsx/package.rb
index 1af5761b..8d124ba3 100644
--- a/lib/axlsx/package.rb
+++ b/lib/axlsx/package.rb
@@ -4,6 +4,7 @@ module Axlsx
# xlsx document including valdation and serialization.
class Package
+ # plain text password
# provides access to the app doc properties for this package
# see App
attr_reader :app
@@ -87,6 +88,11 @@ module Axlsx
true
end
+ # Encrypt the package into a CFB using the password provided
+ def encrypt(file_name, password)
+ moc = MsOffCrypto.new(file_name, password)
+ moc.save
+ end
# Validate all parts of the package against xsd schema.
# @return [Array] An array of all validation errors found.
diff --git a/lib/axlsx/util/cbf.rb b/lib/axlsx/util/cbf.rb
new file mode 100644
index 00000000..b1725457
--- /dev/null
+++ b/lib/axlsx/util/cbf.rb
@@ -0,0 +1,252 @@
+module Axlsx
+
+ # The Cfb class is a MS-OFF-CRYPTOGRAPHY specific OLE (MS-CBF) writer implementation. No attempt is made to re-invent the wheel for read/write of compound binary files.
+ class Cbf
+
+ # the serialization for the CBF FAT
+ FAT_PACKING = "s128"
+
+ # the serialization for the MS-OFF-CRYPTO version stream
+ VERSION_PACKING = 'l s30 l3'
+
+ # The serialization for the MS-OFF-CRYPTO dataspace map stream
+ DATA_SPACE_MAP_PACKING = 'l6 s16 l s25'
+
+ # The serialization for the MS-OFF-CRYPTO strong encrytion data space stream
+ STRONG_ENCRYPTION_DATA_SPACE_PACKING = 'l3 s25'
+
+ # The serialization for the MS-OFF-CRYPTO primary stream
+ PRIMARY_PACKING = 'l3 s38 l s39 l3 x12 l x2'
+
+ # The cutoff size that determines if a stream should be in the mini-fat or the fat
+ MINI_CUTOFF = 4096
+
+ # The serialization for CBF header
+ HEADER_PACKING = "q x16 l s3 x10 l l x4 l*"
+
+ # Creates a new Cbf object based on the ms_off_crypto object provided.
+ # @param [MsOffCrypto] ms_off_crypto
+ def initialize(ms_off_crypto)
+ @file_name = ms_off_crypto.file_name
+ create_storages
+ mini_fat_stream
+ mini_fat
+ fat
+ header
+ end
+
+ # creates or returns the version storage
+ # @return [Storage]
+ def version
+ @version ||= create_version
+ end
+
+ # returns the data space map storage
+ # @return [Storage]
+ def data_space_map
+ @data_space_map ||= create_data_space_map
+ end
+
+ # returns the primary storage
+ # @return [Storgae]
+ def primary
+ @primary ||= create_primary
+ end
+
+ # returns the stream of data allocated in the fat
+ # @return [String]
+ def fat_stream
+ @fat_stream ||= create_fat_stream
+ end
+
+ # returns the stream allocated in the mini fat.
+ # return [String]
+ def mini_fat_stream
+ @mini_fat_stream ||= create_mini_fat_stream
+ end
+
+ # returns the mini fat
+ # return [String]
+ def mini_fat
+ @mini_fat ||= create_mini_fat
+ end
+
+ # returns the fat
+ # @return [String]
+ def fat
+ @fat ||= create_fat
+ end
+
+ # returns the CFB header
+ # @return [String]
+ def header
+ @header ||= create_header
+ end
+
+
+ # writes the compound binary file to disk
+ def save
+ ole = File.open(@file_name, 'w')
+ ole << header
+ ole << fat
+ @storages.each { |s| ole << s.to_s }
+ ole << Array.new((512-(ole.pos % 512)), 0).pack('c*')
+ ole << mini_fat
+ ole << mini_fat_stream
+ ole << fat_stream
+ ole.close
+ end
+
+ private
+
+ # Generates the storages required for ms-office-cryptography cfb
+ def create_storages
+ @storages = []
+ @encryption_info = ms_off_crypto.encryption_info
+ @encrypted_package = ms_off_crypto.encrypted_package
+ @storages << Storage.new('R', :type=>Storage::TYPES[:root], :color=>Storage::COLORS[:red], :child=>1, :modified=>129685612742510730)
+ @storages.last.name_size = 2
+ @storages << Storage.new('EncryptionInfo', :data=>@encryption_info, :left=>3, :size => @encryption_info.size) # example shows right child. do we need the summary info????
+ @storages << Storage.new('EncryptedPackage', :data=>@encrypted_package, :color=>Storage::COLORS[:red], :size=>@encrypted_package.size)
+ @storages << Storage.new([6].pack("c")+"DataSpaces", :child=>5, :modified =>129685612740945580, :created=>129685612740819979)
+ @storages << version
+ @storages << data_space_map
+ @storages << Storage.new('DataSpaceInfo', :right=>8, :child=>7, :created=>129685612740828880,:modified=>129685612740831800)
+ @storages << strong_encryption_data_space
+ @storages << Storage.new('TransformInfo', :color => Storage::COLORS[:red], :child=>9, :created=>129685612740834130, :modified=>129685612740943959)
+ @storages << Storage.new('StrongEncryptionTransform', :child=>10, :created=>129685612740834169, :modified=>129685612740942280)
+ @storages << primary
+ end
+
+ # generates the mini fat stream
+ # @return [String]
+ def create_mini_fat_stream
+ mfs = []
+ @storages.select{ |s| s.type == Storage::TYPES[:stream] && s.size < MINI_CUTOFF}.each_with_index do |stream, index|
+ mfs.concat stream.data
+ mfs.concat Array.new(64 - (mfs.size % 64), 0) if mfs.size % 64
+ end
+ @storages[0].size = mfs.size
+ mfs.concat(Array.new(512 - (mfs.size % 512), 0))
+ mfs.pack 'c*'
+ end
+
+ # generates the fat stream.
+ # @return [String]
+ def create_fat_stream
+ mfs = []
+ @storages.select{ |s| s.type == Storage::TYPES[:stream] && s.size >= MINI_CUTOFF}.each_with_index do |stream, index|
+ mfs.concat stream.data
+ mfs.concat Array.new(512 - (mfs.size % 512), 0) if mfs.size % 512
+ end
+ mfs.pack 'c*'
+ end
+
+ # creates the mini fat
+ # @return [String]
+ def create_mini_fat
+ v_mf = []
+ @storages.select{ |s| s.type == Storage::TYPES[:stream] && s.size < MINI_CUTOFF}.each do |stream|
+ allocate_stream(v_mf, stream, 64)
+ end
+ v_mf.concat Array.new(128 - v_mf.size, -1)
+ v_mf.pack 'l*'
+ end
+
+ # creates the fat
+ # @return [String]
+ def create_fat
+ v_fat = [-3]
+ # storages four per sector, allocation forces directories to start at sector ID 0
+ allocate_stream(v_fat, @storages, 4)
+ # fat entry for minifat
+ allocate_stream(v_fat, 0, 512)
+ # fat entry for minifat stream
+ @storages[0].sector = v_fat.size
+ allocate_stream(v_fat, mini_fat_stream, 512)
+ # fat entries for encrypted package storage
+ # what to do about DIFAT for larger packages...
+ if @encrypted_package.size > (109 - v_fat.size) * 512
+ raise ArgumentError, "Your package is too big!"
+ end
+
+ if @encrypted_package.size >= MINI_CUTOFF
+ allocate_stream(v_fat, @encrypted_package, 512)
+ end
+
+ v_fat.concat Array.new(128 - v_fat.size, -1) if v_fat.size < 128 #pack in unused sectors
+ v_fat.pack 'l*'
+ end
+
+ # Creates the version storage
+ # @return [Storage]
+ def create_version
+ v_stream= [60, "Microsoft.Container.DataSpaces".bytes.to_a, 1, 1, 1].flatten!.pack VERSION_PACKING
+ Storage.new('Version', :data=>v_stream, :size=>v_stream.size)
+ end
+
+ # returns the strong encryption data space storage
+ # @return [Storgae]
+ def strong_encryption_data_space
+ @strong_encryption_data_space ||= create_strong_encryption_data_space
+ end
+
+ # Creates the data space map storage
+ # @return [Storgae]
+ def create_data_space_map
+ v_stream = [8,1,104, 1,0, 32, "EncryptedPackage".bytes.to_a, 50, "StrongEncryptionDataSpace".bytes.to_a].flatten!.pack DATA_SPACE_MAP_PACKING
+ Storage.new('DataSpaceMap', :data=>v_stream, :left => 4, :right => 6, :size=>v_stream.size)
+ end
+
+
+ # creates the stron encryption data space storage
+ # @return [Storgae]
+ def create_strong_encryption_data_space
+ v_stream = [8,1,50,"StrongEncryptionTransform".bytes.to_a].flatten.pack STRONG_ENCRYPTION_DATA_SPACE_PACKING
+ Storage.new("StrongEncryptionDataSpace", :data=>v_stream, :size => v_stream.size)
+ end
+
+ # creates the primary storage
+ # @return [Storgae]
+ def create_primary
+ v_stream = [88,1,76,"{FF9A3F03-56EF-4613-BDD5-5A41C1D07246}".bytes.to_a].flatten
+ v_stream.concat [78, "Microsoft.Container.EncryptionTransform".bytes.to_a,1,1,1,4].flatten
+ v_stream = v_stream.pack PRIMARY_PACKING
+ Storage.new([6].pack("c")+"Primary", :data=>v_stream, :size=>v_stream.size)
+ end
+
+ # Creates the header
+ # @return [String]
+ def create_header
+ header = []
+ header << -2226271756974174256 # identifier pack as q
+ header << 196670 # version pack as L
+ header << 65534 # byte order pack as s
+ header << 9 # sector shift
+ header << 6 # mini-sector shift
+ header << (fat.size/512.0).ceil # this is the number of FAT sectors in the file at index 6 pack as L
+ header << header.last # this is the first directory sector, index of 7 pack as L
+ header << MINI_CUTOFF # minfat cutoff pack as L
+ # MiniFat starts after directories
+ header << (fat.size/512.0).ceil + (@storages.size/4.0).ceil # this is the sector id for the first minifat index 10 pack as L
+ header << (mini_fat.size/512.0).ceil # minifat sector count index 11 pack as L
+ header << -2 # the first DIFAT - set to end of chain until we exceed a single FAT pack as L
+ header << 0 # number of DIFAT sectors, unless we go beyond 109 FAT sectors this will always be 0 pack as L
+ header << 0 # first FAT sector defined in the DIFAT pack as L
+ header.concat Array.new(108, -1) # Difat sectors pack as L108
+ header.pack(HEADER_PACKING)
+ end
+
+ # Allocates sector chains in a allocation table based on the sector size and stream provided
+ # If a storage obeject is provided, the starting sector value for the storage is updated based on the allocation performed here.
+ # @param [Array] table Allocation table array
+ # @param [Storage | String] stream
+ # @param [Integer] size The cutoff size for the stream.
+ def allocate_stream(table, stream, size)
+ stream.sector = table.size if stream.respond_to?(:sector)
+ ((stream.size / size.to_f).ceil).times { table << table.size }
+ table[table.size-1] = -2 # this is the CBF chain terminator
+ end
+
+ end
+end
diff --git a/lib/axlsx/util/ms_off_crypto.rb b/lib/axlsx/util/ms_off_crypto.rb
index 7f20ac28..486f8d70 100644
--- a/lib/axlsx/util/ms_off_crypto.rb
+++ b/lib/axlsx/util/ms_off_crypto.rb
@@ -1,61 +1,152 @@
+# -*- coding: utf-8 -*-
require 'digest'
require 'base64'
require 'openssl'
+
module Axlsx
+
+ # The MsOffCrypto class implements ECMA-367 encryption based on the MS-OFF-CRYPTO specification
class MsOffCrypto
- attr_reader :verifier
- attr_reader :key
+ # Creates a new MsOffCrypto Object
+ # @param [String] file_name the location of the file you want to encrypt
+ # @param [String] pwd the password to use when encrypting the file
+ def initialize(file_name, pwd)
+ self.password = pwd
+ self.file_name = file_name
+ end
+
+ # Generates a new CBF file based on this instance of ms-off-crypto and overwrites the unencrypted file.
+ def save
+ cfb = Cbf.new(self)
+ cfb.save
+ end
- def initialize(password = "passowrd")
- @password = password
- @salt_size = 0x10
- @key_size = 0x100
- @verifier = rand(16**16).to_s
+ # returns the raw password used in encryption
+ # @return [String]
+ attr_reader :password
- #fixed salt for testing
- @salt = [0x90,0xAC,0x68,0x0E,0x76,0xF9,0x43,0x2B,0x8D,0x13,0xB7,0x1D,0xB7,0xC0,0xFC,0x0D].join
- # @salt =Digest::SHA1.digest(rand(16**16).to_s)
+ # sets the password to be used for encryption
+ # @param [String] v the password, @default 'password'
+ # @return [String]
+ def password=(v)
+ @password = v || 'password'
end
- def encryption_info
- # v.major v.minor flags header length flags size # AES 128 bit
- header = [3, 0, 2, 0, 0x24, 0, 0, 0, 0xA4, 0, 0, 0, 0x24, 0, 0, 0, 0, 0, 0, 0, 0x0E, 0x66, 0, 0]
- header.concat [0x04, 0x80, 0, 0, 0x80, 0, 0, 0, 0x18, 0, 0, 0, 0xA0, 0xC7, 0xDC, 0x2, 0, 0, 0, 0]
- header.concat "Microsoft Enhanced RSA and AES Cryptographic Provider (Prototype)".bytes.to_a.pack('s*').bytes.to_a
- header.concat [0, 0]
- header.concat [0x10, 0, 0, 0]
- header.concat [0x90,0xAC,0x68,0x0E,0x76,0xF9,0x43,0x2B,0x8D,0x13,0xB7,0x1D,0xB7,0xC0,0xFC,0x0D]
- header.concat encrypted_verifier.bytes.to_a.pack('c*').bytes.to_a
- header.concat [20, 0,0,0]
- header.concat encrypted_verifier_hash.bytes.to_a.pack('c*').bytes.to_a
- header.flatten!
- header.pack('c*')
+ # retruns the file name of the archive to be encrypted
+ # @return [String]
+ attr_reader :file_name
+
+ # sets the filename
+ # @return [String]
+ def file_name=(v)
+ #TODO verfify that the file specified exists and is an unencrypted xlsx archive
+ @file_name = v
end
- def encryption_verifier
- {:salt_size => @salt_size,
- :salt => @salt,
- :encrypted_verifier => encrypted_verifier,
- :varifier_hash_size => 0x14,
- :encrypted_verifier_hash => encrypted_verifier_hash}
+
+ # encrypts and returns the package specified by the file name
+ # @return [String]
+ def encrypted_package
+ @encrypted_package ||= encrypt_package(file_name, password)
end
- # 2.3.3
+ # returns the encryption info for this instance of ms-off-crypto
+ # @return [String]
+ def encryption_info
+ @encryption_info ||= create_encryption_info
+ end
+
+ # returns a random salt
+ # @return [String]
+ def salt
+ @salt ||= Digest::SHA1.digest(rand(16**16).to_s)
+ end
+
+ # returns a random verifier
+ # @return [String]
+ def verifier
+ @verifier ||= rand(16**16).to_s
+ end
+
+ # returns the verifier encrytped
+ # @return [String]
def encrypted_verifier
- @encrypted_verifier ||= encrypt(@verifier)
+ @encrypted_verifier ||= encrypt(verifier)
end
- # 2.3.3
+ # returns the verifier hash encrypted
+ # @return [String]
def encrypted_verifier_hash
- verifier_hash = Digest::SHA1.digest(@verifier)
- verifier_hash << Array.new(32 - verifier_hash.size, 0).join('')
@encrypted_verifier_hash ||= encrypt(verifier_hash)
end
+ # returns a verifier hash
+ # @return [String]
+ def verifier_hash
+ @verifier_hash ||= create_verifier_hash
+ end
+
+ # returns an encryption key
+ # @return [String]
+ def key
+ @key ||= create_key
+ end
+
+ # size of unencrypted package? concated with encrypted package
+ def encrypt_package(file_name, password)
+ package = File.open(file_name, 'r')
+ package_text = package.read
+ [package_text.bytes.to_a.size].pack('q') + encrypt(package_text)
+ end
+
+ # Generates an encryption info structure
+ # @return [String]
+ def create_encryption_info
+ header = [3, 0, 2, 0] # version
+ # Header flags copy
+ header.concat [0x24, 0, 0, 0] #flags -- VERY UNSURE ABOUT THIS STILL
+ header.concat [0, 0, 0, 0] #unused
+ header.concat [0xA4, 0, 0, 0] #length
+ # Header
+ header.concat [0x24, 0, 0, 0] #flags again
+ header.concat [0, 0, 0, 0] #unused again,
+ header.concat [0x0E, 0x66, 0, 0] #alg id
+ header.concat [0x04, 0x80, 0, 0] #alg hash id
+ header.concat [key.size, 0, 0, 0] #key size
+ header.concat [0x18, 0, 0, 0] #provider type
+ header.concat [0, 0, 0, 0] #reserved 1
+ header.concat [0, 0, 0, 0] #reserved 2
+ #header.concat [0xA0, 0xC7, 0xDC, 0x2, 0, 0, 0, 0]
+ header.concat "Microsoft Enhanced RSA and AES Cryptographic Provider (Prototype)".bytes.to_a.pack('s*').bytes.to_a
+ header.concat [0, 0] #null terminator
+
+ #Salt Size
+ header.concat [salt.bytes.to_a.size].pack('l').bytes.to_a
+ #Salt
+ header.concat salt.bytes.to_a.pack('c*').bytes.to_a
+ # encryption verifier
+ header.concat encrypted_verifier.bytes.to_a.pack('c*').bytes.to_a
+
+ # verifier hash size -- MUST BE 32 bytes
+ header.concat [verifier_hash.bytes.to_a.size].pack('l').bytes.to_a
+
+ #encryption verifier hash
+ header.concat encrypted_verifier_hash.bytes.to_a.pack('c*').bytes.to_a
+
+ header.flatten!
+ header.pack('c*')
+ end
+
+ # 2.3.3
+ def create_verifier_hash
+ vh = Digest::SHA1.digest(verifier)
+ vh << Array.new(32 - vh.size, 0).join('')
+ end
+
# 2.3.4.7 ECMA-376 Document Encryption Key Generation (Standard Encryption)
- def key
- sha = Digest::SHA1.new() << (@salt + @password)
+ def create_key
+ sha = Digest::SHA1.new() << (salt + @password)
(0..49999).each { |i| sha.update(i.to_s+sha.to_s) }
key = sha.update(sha.to_s+'0').digest
a = key.bytes.each_with_index.map { |item, i| 0x36 ^ item }
@@ -63,25 +154,35 @@ module Axlsx
a = key.bytes.each_with_index.map { |item, i| 0x5C ^ item }
x2 = Digest::SHA1.digest( (a.concat Array.new(64 - key.size, 0x5C) ).to_s)
x3 = x1 + x2
- @key ||= x3.bytes.to_a[(0..31)].pack('c*')
+ x3.bytes.to_a[(0..31)].pack('c*')
end
+ # ensures that the a hashed decryption of the encryption verifier matches the decrypted verifier hash.
+ # @return [Boolean]
def verify_password
- puts decrypt(@encrypted_verifier)
+ v = Digest::SHA1.digest decrypt(@encrypted_verifier)
+ vh = decrypt(@encrypted_verifier_hash)
+ vh[0..15] == v[0..15]
end
+ # encrypts the data proved
+ # @param [String] data
+ # @return [String] the encrypted data
def encrypt(data)
aes = OpenSSL::Cipher.new("AES-128-ECB")
aes.encrypt
aes.key = key
- aes.update(data)
+ aes.update(data) << aes.final
end
+ # dencrypts the data proved
+ # @param [String] data
+ # @return [String] the dencrypted data
def decrypt(data)
aes = OpenSSL::Cipher.new("AES-128-ECB")
aes.decrypt
aes.key = key
- aes.update(data)
+ aes.update(data) << aes.final
end
end
diff --git a/lib/axlsx/util/storage.rb b/lib/axlsx/util/storage.rb
new file mode 100644
index 00000000..6841d3d7
--- /dev/null
+++ b/lib/axlsx/util/storage.rb
@@ -0,0 +1,146 @@
+module Axlsx
+
+ # The Storage class represents a storage object or stream in a compound file.
+ class Storage
+
+ # Packing for the Storage when pushing an array of items into a byte stream
+ # Name, name length, type, color, left sibling, right sibling, child, classid, state, created, modified, sector, size
+ PACKING = "s32 s1 c2 l3 x16 x4 q2 l q"
+
+ # storage types
+ TYPES = {
+ :root=>5,
+ :stream=>2,
+ :storage=>1
+ }
+
+ # Creates a byte string for this storage
+ # @return [String]
+ def to_s
+ data = [@name.concat(Array.new([email protected], 0)),
+ @name_size,
+ @type,
+ @color,
+ @left,
+ @right,
+ @child,
+ @created,
+ @modified,
+ @sector,
+ @size].flatten
+ puts data.inspect
+ data.pack(PACKING)
+ end
+
+ # storage colors
+ COLORS = {
+ :red=>0,
+ :black=>1
+ }
+
+ # The color of this node in the directory tree. Defaults to black if not specified
+ # @return [Integer] color
+ attr_reader :color
+
+ # Sets the color for this storage
+ # @param [Integer] v Must be one of the COLORS constant hash values
+ def color=(v)
+ RestrictionValidator.validate "Storage.color", COLORS.values, v
+ @color = v
+ end
+
+ # The size of the name for this node.
+ # interesting to see that office actually uses 'R' for the root directory and lists the size as 2 bytes - thus is it *NOT* null
+ # terminated. I am making this r/w so that I can override the size
+ # @return [Integer] color
+ attr_reader :name_size
+
+ # the name of the stream
+ attr_reader :name
+
+ # sets the name of the stream.
+ # This will automatically set the name_size attribute
+ # @return [String] name
+ def name=(v)
+ @name = v.bytes.to_a << 0
+ @name_size = @name.size * 2
+ @name
+ end
+
+ # The size of the stream
+ attr_reader :size
+
+ # The stream associated with this storage
+ attr_reader :data
+
+ # Set the data associated with the stream. If the stream type is undefined, we automatically specify the storage as a stream type. # with the exception of storages that are type root, all storages with data should be type stream.
+ # @param [String] v The data for this storages stream
+ # @return [Array]
+ def data=(v)
+ Axlsx::validate_string(v)
+ self.type = TYPES[:stream] unless @type
+ @size = v.size
+ @data = v.bytes.to_a
+ end
+
+ # The starting sector for the stream. If this storage is not a stream, or the root node this is nil
+ # @return [Integer] sector
+ attr_accessor :sector
+
+ # The 0 based index in the directoies chain for this the left sibling of this storage.
+
+ # @return [Integer] left
+ attr_accessor :left
+
+ # The 0 based index in the directoies chain for this the right sibling of this storage.
+ # @return [Integer] right
+ attr_accessor :right
+
+ # The 0 based index in the directoies chain for the child of this storage.
+ # @return [Integer] child
+ attr_accessor :child
+
+ # The created attribute for the storage
+ # @return [Integer] created
+ attr_accessor :created
+
+ # The modified attribute for the storage
+ # @return [Integer] modified
+ attr_accessor :modified
+
+ # The type of storage
+ # see TYPES
+ # @return [Integer] type
+ attr_reader :type
+
+ # Sets the type for this storage.
+ # @param [Integer] v the type to specify must be one of the TYPES constant hash values.
+ def type=(v)
+ RestrictionValidator.validate "Storage.type", TYPES.values, v
+ @type = v
+ end
+
+ # Creates a new storage object.
+ # @param [String] name the name of the storage
+ # @option options [Integer] color @default black
+ # @option options [Integer] type @default storage
+ # @option options [String] data
+ # @option options [Integer] left @default -1
+ # @option options [Integer] right @default -1
+ # @option options [Integer] child @default -1
+ # @option options [Integer] created @default 0
+ # @option options [Integer] modified @default 0
+ # @option options [Integer] sector @default 0
+ def initialize(name, options= {})
+ @left = @right = @child = -1
+ @sector = @size = @created = @modified = 0
+ options.each do |o|
+ self.send("#{o[0]}=", o[1]) if self.respond_to? "#{o[0]}="
+ end
+ @color ||= COLORS[:black]
+ @type ||= (data.nil? ? TYPES[:storage] : TYPES[:stream])
+ self.name = name
+ end
+
+ end
+end