openc3-cosmos-cfdp 1.0.2 → 2.0.0

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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -1
  3. data/microservices/CFDP/Gemfile +3 -11
  4. data/microservices/CFDP/Gemfile.dev +35 -0
  5. data/microservices/CFDP/Gemfile.prod +29 -0
  6. data/microservices/CFDP/app/controllers/cfdp_controller.rb +8 -0
  7. data/microservices/CFDP/app/models/cfdp_crc_checksum.rb +3 -1
  8. data/microservices/CFDP/app/models/cfdp_mib.rb +2 -1
  9. data/microservices/CFDP/app/models/cfdp_pdu.rb +43 -19
  10. data/microservices/CFDP/app/models/cfdp_receive_transaction.rb +7 -2
  11. data/microservices/CFDP/app/models/cfdp_source_transaction.rb +6 -1
  12. data/microservices/CFDP/app/models/cfdp_transaction.rb +1 -1
  13. data/microservices/CFDP/app/models/cfdp_user.rb +14 -4
  14. data/microservices/CFDP/config/application.rb +0 -11
  15. data/microservices/CFDP/config/environments/test.rb +1 -1
  16. data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_file_data.rb +6 -2
  17. data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_finished.rb +17 -5
  18. data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_metadata.rb +28 -11
  19. data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_tlv.rb +5 -2
  20. data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_user_ops.rb +16 -9
  21. data/microservices/CFDP/spec/controllers/cfdp_controller_spec.rb +373 -0
  22. data/microservices/CFDP/spec/models/cfdp_crc_checksum_spec.rb +134 -0
  23. data/microservices/CFDP/spec/models/cfdp_mib_spec.rb +389 -0
  24. data/microservices/CFDP/spec/models/cfdp_pdu_metadata_spec.rb +102 -1
  25. data/microservices/CFDP/spec/models/cfdp_pdu_user_ops_spec.rb +562 -0
  26. data/microservices/CFDP/spec/models/cfdp_receive_transaction_spec.rb +598 -0
  27. data/microservices/CFDP/spec/models/cfdp_transaction_spec.rb +331 -0
  28. data/microservices/CFDP/spec/models/cfdp_user_spec.rb +575 -0
  29. metadata +15 -6
@@ -0,0 +1,389 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2025 OpenC3, Inc.
4
+ # All Rights Reserved.
5
+ #
6
+ # Licensed for Evaluation and Educational Use
7
+ #
8
+ # This file may only be used commercially under the terms of a commercial license
9
+ # purchased from OpenC3, Inc.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14
+ #
15
+ # The development of this software was funded in-whole or in-part by Sandia National Laboratories.
16
+ # See https://github.com/OpenC3/openc3-cosmos-cfdp/pull/12 for details
17
+
18
+ require 'rails_helper'
19
+ require 'tempfile'
20
+
21
+ RSpec.describe CfdpMib do
22
+ before(:each) do
23
+ allow(OpenC3::Logger).to receive(:info)
24
+ allow(OpenC3::Logger).to receive(:error)
25
+ CfdpMib.clear
26
+ end
27
+
28
+ describe "entity management" do
29
+ it "defines an entity with default values" do
30
+ entity = CfdpMib.define_entity(1)
31
+ expect(entity['id']).to eq(1)
32
+ expect(entity['protocol_version_number']).to eq(1)
33
+ expect(entity['default_transmission_mode']).to eq('UNACKNOWLEDGED')
34
+ expect(entity['fault_handler']["FILE_CHECKSUM_FAILURE"]).to eq("IGNORE_ERROR")
35
+ end
36
+
37
+ it "returns entity by id" do
38
+ CfdpMib.define_entity(1)
39
+ entity = CfdpMib.entity(1)
40
+ expect(entity['id']).to eq(1)
41
+ end
42
+
43
+ it "sets source entity id" do
44
+ CfdpMib.define_entity(1)
45
+ CfdpMib.source_entity_id = 1
46
+ expect(CfdpMib.source_entity_id).to eq(1)
47
+ end
48
+
49
+ it "returns source entity" do
50
+ CfdpMib.define_entity(1)
51
+ CfdpMib.source_entity_id = 1
52
+ entity = CfdpMib.source_entity
53
+ expect(entity['id']).to eq(1)
54
+ end
55
+ end
56
+
57
+ describe "set_entity_value" do
58
+ before(:each) do
59
+ CfdpMib.define_entity(1)
60
+ end
61
+
62
+ it "sets integer values" do
63
+ CfdpMib.set_entity_value(1, 'protocol_version_number', 2)
64
+ expect(CfdpMib.entity(1)['protocol_version_number']).to eq(2)
65
+ end
66
+
67
+ it "sets boolean values" do
68
+ CfdpMib.set_entity_value(1, 'immediate_nak_mode', false)
69
+ expect(CfdpMib.entity(1)['immediate_nak_mode']).to eq(false)
70
+ end
71
+
72
+ it "sets transmission mode values" do
73
+ CfdpMib.set_entity_value(1, 'default_transmission_mode', 'ACKNOWLEDGED')
74
+ expect(CfdpMib.entity(1)['default_transmission_mode']).to eq('ACKNOWLEDGED')
75
+ end
76
+
77
+ it "sets cmd_info values" do
78
+ CfdpMib.set_entity_value(1, 'cmd_info', ['TARGET', 'PACKET', 'ITEM'])
79
+ expect(CfdpMib.entity(1)['cmd_info']).to eq(['TARGET', 'PACKET', 'ITEM'])
80
+ end
81
+
82
+ it "adds tlm_info values" do
83
+ CfdpMib.set_entity_value(1, 'tlm_info', ['TARGET', 'PACKET', 'ITEM'])
84
+ expect(CfdpMib.entity(1)['tlm_info']).to eq([['TARGET', 'PACKET', 'ITEM']])
85
+ end
86
+
87
+ it "raises an error for unknown options" do
88
+ expect { CfdpMib.set_entity_value(1, 'unknown_option', 'value') }.to raise_error(RuntimeError, /Unknown OPTION/)
89
+ end
90
+
91
+ it "raises an error for invalid tlm_info" do
92
+ expect { CfdpMib.set_entity_value(1, 'tlm_info', ['TARGET', 'PACKET']) }.to raise_error(RuntimeError, /Invalid tlm_info/)
93
+ end
94
+
95
+ it "raises an error for invalid cmd_info" do
96
+ expect { CfdpMib.set_entity_value(1, 'cmd_info', ['TARGET', 'PACKET']) }.to raise_error(RuntimeError, /Invalid cmd_info/)
97
+ end
98
+ end
99
+
100
+ describe "file operations" do
101
+ before(:each) do
102
+ CfdpMib.root_path = "/tmp"
103
+ @tmp_file = Tempfile.new('cfdp_test')
104
+ @tmp_file.write("test data")
105
+ @tmp_file.close
106
+ end
107
+
108
+ after(:each) do
109
+ @tmp_file.unlink if @tmp_file
110
+ end
111
+
112
+ it "gets a source file" do
113
+ allow(File).to receive(:open).and_return(@tmp_file)
114
+ file = CfdpMib.get_source_file("test.txt")
115
+ expect(file).to eq(@tmp_file)
116
+ end
117
+
118
+ it "handles missing source files" do
119
+ allow(File).to receive(:open).and_raise(Errno::ENOENT.new("No such file"))
120
+ file = CfdpMib.get_source_file("missing.txt")
121
+ expect(file).to be_nil
122
+ end
123
+
124
+ it "completes a source file" do
125
+ allow(@tmp_file).to receive(:close)
126
+ CfdpMib.complete_source_file(@tmp_file)
127
+ # Just verifying no errors are raised
128
+ end
129
+
130
+ it "puts a destination file" do
131
+ # Create a fake tempfile for testing
132
+ temp = Tempfile.new('cfdp_dest')
133
+ allow(temp).to receive(:persist).and_return(true)
134
+ allow(temp).to receive(:unlink).and_return(true)
135
+ allow(temp).to receive(:open).and_return(temp)
136
+ allow(temp).to receive(:read).and_return("test data")
137
+
138
+ result = CfdpMib.put_destination_file("test_dest.txt", temp)
139
+ expect(result).to be true
140
+ end
141
+
142
+ it "handles errors while putting destination files" do
143
+ temp = Tempfile.new('cfdp_dest')
144
+ allow(temp).to receive(:persist).and_raise("File write error")
145
+
146
+ result = CfdpMib.put_destination_file("test_dest.txt", temp)
147
+ expect(result).to be false
148
+ end
149
+ end
150
+
151
+ describe "filestore_request" do
152
+ before(:each) do
153
+ CfdpMib.root_path = "/tmp"
154
+ @tmp_file1 = Tempfile.new('cfdp_test1')
155
+ @tmp_file1.write("test data 1")
156
+ @tmp_file1.close
157
+
158
+ @tmp_file2 = Tempfile.new('cfdp_test2')
159
+ @tmp_file2.write("test data 2")
160
+ @tmp_file2.close
161
+
162
+ # Setup mocks for File operations
163
+ allow(File).to receive(:absolute_path) { |path| path }
164
+ allow(File).to receive(:exist?).and_return(true)
165
+ allow(FileUtils).to receive(:touch).and_return(true)
166
+ allow(FileUtils).to receive(:rm).and_return(true)
167
+ allow(FileUtils).to receive(:mv).and_return(true)
168
+ allow(FileUtils).to receive(:mkdir).and_return(true)
169
+ allow(FileUtils).to receive(:rmdir).and_return(true)
170
+ allow(File).to receive(:open).and_yield(StringIO.new)
171
+ allow(File).to receive(:read).and_return("test data")
172
+ allow(Dir).to receive(:exist?).and_return(true)
173
+ end
174
+
175
+ after(:each) do
176
+ @tmp_file1.unlink if @tmp_file1
177
+ @tmp_file2.unlink if @tmp_file2
178
+ end
179
+
180
+ it "handles CREATE_FILE action" do
181
+ status, message = CfdpMib.filestore_request("CREATE_FILE", "test_create.txt", nil)
182
+ expect(status).to eq("SUCCESSFUL")
183
+ end
184
+
185
+ it "handles DELETE_FILE action when file exists" do
186
+ status, message = CfdpMib.filestore_request("DELETE_FILE", "test_delete.txt", nil)
187
+ expect(status).to eq("SUCCESSFUL")
188
+ end
189
+
190
+ it "handles DELETE_FILE action when file doesn't exist" do
191
+ allow(File).to receive(:exist?).and_return(false)
192
+ status, message = CfdpMib.filestore_request("DELETE_FILE", "missing.txt", nil)
193
+ expect(status).to eq("FILE_DOES_NOT_EXIST")
194
+ end
195
+
196
+ it "handles RENAME_FILE action" do
197
+ allow(File).to receive(:exist?).with("/tmp/old.txt").and_return(true)
198
+ allow(File).to receive(:exist?).with("/tmp/new.txt").and_return(false)
199
+ status, message = CfdpMib.filestore_request("RENAME_FILE", "old.txt", "new.txt")
200
+ expect(status).to eq("SUCCESSFUL")
201
+ end
202
+
203
+ it "handles RENAME_FILE when destination already exists" do
204
+ # First call to exist? for the destination file
205
+ allow(File).to receive(:exist?).with("/tmp/new.txt").and_return(true)
206
+
207
+ status, message = CfdpMib.filestore_request("RENAME_FILE", "old.txt", "new.txt")
208
+ expect(status).to eq("NEW_FILE_ALREADY_EXISTS")
209
+ end
210
+
211
+ it "handles APPEND_FILE action" do
212
+ status, message = CfdpMib.filestore_request("APPEND_FILE", "file1.txt", "file2.txt")
213
+ expect(status).to eq("SUCCESSFUL")
214
+ end
215
+
216
+ it "handles REPLACE_FILE action" do
217
+ status, message = CfdpMib.filestore_request("REPLACE_FILE", "file1.txt", "file2.txt")
218
+ expect(status).to eq("SUCCESSFUL")
219
+ end
220
+
221
+ it "handles CREATE_DIRECTORY action" do
222
+ status, message = CfdpMib.filestore_request("CREATE_DIRECTORY", "new_dir", nil)
223
+ expect(status).to eq("SUCCESSFUL")
224
+ end
225
+
226
+ it "handles REMOVE_DIRECTORY action" do
227
+ status, message = CfdpMib.filestore_request("REMOVE_DIRECTORY", "test_dir", nil)
228
+ expect(status).to eq("SUCCESSFUL")
229
+ end
230
+
231
+ it "handles DENY_FILE action" do
232
+ status, message = CfdpMib.filestore_request("DENY_FILE", "test_file.txt", nil)
233
+ expect(status).to eq("SUCCESSFUL")
234
+ end
235
+
236
+ it "handles DENY_DIRECTORY action" do
237
+ status, message = CfdpMib.filestore_request("DENY_DIRECTORY", "test_dir", nil)
238
+ expect(status).to eq("SUCCESSFUL")
239
+ end
240
+
241
+ it "handles unknown action codes" do
242
+ status, message = CfdpMib.filestore_request("UNKNOWN_ACTION", "file.txt", nil)
243
+ expect(status).to eq("NOT_PERFORMED")
244
+ expect(message).to include("Unknown action code")
245
+ end
246
+
247
+ it "handles file path safety" do
248
+ allow(File).to receive(:absolute_path).with("/tmp/../dangerous.txt").and_return("/dangerous.txt")
249
+
250
+ status, message = CfdpMib.filestore_request("CREATE_FILE", "../dangerous.txt", nil)
251
+ expect(status).to eq("NOT_ALLOWED")
252
+ expect(message).to include("Dangerous filename")
253
+ end
254
+
255
+ it "handles exceptions during file operations" do
256
+ allow(FileUtils).to receive(:touch).and_raise("File system error")
257
+
258
+ status, message = CfdpMib.filestore_request("CREATE_FILE", "test.txt", nil)
259
+ expect(status).to eq("NOT_ALLOWED")
260
+ expect(message).to include("File system error")
261
+ end
262
+ end
263
+
264
+ describe "directory_listing" do
265
+ before(:each) do
266
+ CfdpMib.root_path = "/tmp"
267
+
268
+ # Setup mocks
269
+ allow(File).to receive(:absolute_path) { |path| path }
270
+ allow(File).to receive(:join) { |*args| args.join('/') }
271
+ allow(Dir).to receive(:entries).and_return(['.', '..', 'file1.txt', 'file2.txt', 'subdir'])
272
+ allow(File).to receive(:directory?).with("/tmp/test_dir/subdir").and_return(true)
273
+ allow(File).to receive(:directory?).with("/tmp/test_dir/file1.txt").and_return(false)
274
+ allow(File).to receive(:directory?).with("/tmp/test_dir/file2.txt").and_return(false)
275
+
276
+ file_stat = double("File::Stat")
277
+ allow(file_stat).to receive(:mtime).and_return(Time.now)
278
+ allow(file_stat).to receive(:size).and_return(1024)
279
+ allow(File).to receive(:stat).and_return(file_stat)
280
+ end
281
+
282
+ it "returns a JSON listing of files and directories" do
283
+ result = CfdpMib.directory_listing("test_dir", "result.txt")
284
+ expect(result).to be_a(String)
285
+
286
+ # Parse the JSON and verify it contains the expected entries
287
+ json = JSON.parse(result)
288
+ expect(json).to be_an(Array)
289
+ expect(json.size).to eq(3) # file1.txt, file2.txt, subdir
290
+
291
+ # Check for directory and file entries
292
+ dir_entry = json.find { |entry| entry["directory"] == "subdir" }
293
+ expect(dir_entry).to be_present
294
+
295
+ file_entry = json.find { |entry| entry["name"] == "file1.txt" }
296
+ expect(file_entry).to be_present
297
+ expect(file_entry["size"]).to eq(1024)
298
+ end
299
+
300
+ it "handles file path safety" do
301
+ allow(File).to receive(:absolute_path).with("/tmp/../dangerous").and_return("/dangerous")
302
+
303
+ result = CfdpMib.directory_listing("../dangerous", "result.txt")
304
+ expect(result).to be_nil
305
+ end
306
+ end
307
+
308
+ describe "setup" do
309
+ before(:each) do
310
+ @mock_model = double("MicroserviceModel")
311
+ @options = [
312
+ ["source_entity_id", "1"],
313
+ ["destination_entity_id", "2"],
314
+ ["root_path", "/tmp"],
315
+ ["protocol_version_number", "1"],
316
+ ["ack_timer_interval", "300"],
317
+ ["immediate_nak_mode", "true"],
318
+ ["cmd_info", "TARGET", "PACKET", "ITEM"],
319
+ ["tlm_info", "TARGET", "TLM_PKT", "ITEM"]
320
+ ]
321
+ allow(@mock_model).to receive(:options).and_return(@options)
322
+ allow(OpenC3::MicroserviceModel).to receive(:get_model).and_return(@mock_model)
323
+ end
324
+
325
+ it "initializes MIB from options" do
326
+ CfdpMib.setup
327
+
328
+ # Verify entities were created
329
+ expect(CfdpMib.source_entity_id).to eq(1)
330
+ expect(CfdpMib.entity(1)).to be_present
331
+ expect(CfdpMib.entity(2)).to be_present
332
+
333
+ # Verify options were applied
334
+ expect(CfdpMib.entity(2)['protocol_version_number']).to eq(1)
335
+ expect(CfdpMib.entity(2)['ack_timer_interval']).to eq(300)
336
+ expect(CfdpMib.entity(2)['immediate_nak_mode']).to eq(true)
337
+ expect(CfdpMib.entity(2)['cmd_info']).to eq(["TARGET", "PACKET", "ITEM"])
338
+ expect(CfdpMib.entity(2)['tlm_info']).to include(["TARGET", "TLM_PKT", "ITEM"])
339
+
340
+ # Verify root path was set
341
+ expect(CfdpMib.root_path).to eq("/tmp")
342
+ end
343
+
344
+ it "raises error when required options are missing" do
345
+ # Remove required options
346
+ @options.delete_if { |opt| opt[0] == "source_entity_id" }
347
+
348
+ expect { CfdpMib.setup }.to raise_error(RuntimeError, /OPTION source_entity_id is required/)
349
+ end
350
+ end
351
+
352
+ describe "cleanup_old_transactions" do
353
+ before(:each) do
354
+ # Setup entities
355
+ CfdpMib.define_entity(1)
356
+ CfdpMib.source_entity_id = 1
357
+ CfdpMib.entity(1)['transaction_retain_seconds'] = 60
358
+
359
+ # Create mock transactions
360
+ @active_tx = double("Transaction")
361
+ allow(@active_tx).to receive(:complete_time).and_return(nil)
362
+
363
+ @recent_tx = double("Transaction")
364
+ allow(@recent_tx).to receive(:complete_time).and_return(Time.now.utc - 30)
365
+
366
+ @old_tx = double("Transaction")
367
+ allow(@old_tx).to receive(:complete_time).and_return(Time.now.utc - 120)
368
+
369
+ # Add transactions to the MIB
370
+ CfdpMib.transactions["tx1"] = @active_tx
371
+ CfdpMib.transactions["tx2"] = @recent_tx
372
+ CfdpMib.transactions["tx3"] = @old_tx
373
+ end
374
+
375
+ it "removes old completed transactions" do
376
+ # Verify there are 3 transactions before cleanup
377
+ expect(CfdpMib.transactions.size).to eq(3)
378
+
379
+ # Run cleanup
380
+ CfdpMib.cleanup_old_transactions
381
+
382
+ # Verify only old transaction was removed
383
+ expect(CfdpMib.transactions.size).to eq(2)
384
+ expect(CfdpMib.transactions).to have_key("tx1")
385
+ expect(CfdpMib.transactions).to have_key("tx2")
386
+ expect(CfdpMib.transactions).not_to have_key("tx3")
387
+ end
388
+ end
389
+ end
@@ -1,6 +1,6 @@
1
1
  # encoding: ascii-8bit
2
2
 
3
- # Copyright 2023 OpenC3, Inc.
3
+ # Copyright 2025 OpenC3, Inc.
4
4
  # All Rights Reserved.
5
5
  #
6
6
  # Licensed for Evaluation and Educational Use
@@ -13,6 +13,9 @@
13
13
  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14
14
  #
15
15
  # The development of this software was funded in-whole or in-part by MethaneSAT LLC.
16
+ #
17
+ # The development of this software was funded in-part by Sandia National Laboratories.
18
+ # See https://github.com/OpenC3/openc3-cosmos-cfdp/pull/12 for details
16
19
 
17
20
  require 'rails_helper'
18
21
  require 'cfdp_pdu'
@@ -74,6 +77,7 @@ RSpec.describe CfdpPdu, type: :model do
74
77
  expect(buffer[23..26].unpack('A*')[0]).to eql 'test'
75
78
 
76
79
  hash = {}
80
+ hash['VERSION'] = 1
77
81
  # decom takes just the Metadata specific part of the buffer
78
82
  # so start at offset 8 and ignore the 2 checksum bytes
79
83
  CfdpPdu.decom_metadata_pdu_contents(CfdpPdu.new(crcs_required: false), hash, buffer[8..-3])
@@ -154,6 +158,7 @@ RSpec.describe CfdpPdu, type: :model do
154
158
  expect(buffer[46..48].unpack('A*')[0]).to eql 'end'
155
159
 
156
160
  hash = {}
161
+ hash['VERSION'] = 1
157
162
  # decom takes just the Metadata specific part of the buffer
158
163
  # so start at offset 8 and ignore the 2 checksum bytes
159
164
  CfdpPdu.decom_metadata_pdu_contents(CfdpPdu.new(crcs_required: false), hash, buffer[8..-3])
@@ -216,6 +221,7 @@ RSpec.describe CfdpPdu, type: :model do
216
221
  expect(buffer[29..33].unpack('A*')[0]).to eql 'Hello'
217
222
 
218
223
  hash = {}
224
+ hash['VERSION'] = 1
219
225
  # decom takes just the Metadata specific part of the buffer
220
226
  # so start at offset 8 and ignore the 2 checksum bytes
221
227
  CfdpPdu.decom_metadata_pdu_contents(CfdpPdu.new(crcs_required: false), hash, buffer[8..-3])
@@ -274,6 +280,7 @@ RSpec.describe CfdpPdu, type: :model do
274
280
  expect(buffer[29].unpack('C')[0] & 0xF).to eql 4
275
281
 
276
282
  hash = {}
283
+ hash['VERSION'] = 1
277
284
  # decom takes just the Metadata specific part of the buffer
278
285
  # so start at offset 8 and ignore the 2 checksum bytes
279
286
  CfdpPdu.decom_metadata_pdu_contents(CfdpPdu.new(crcs_required: false), hash, buffer[8..-3])
@@ -331,6 +338,7 @@ RSpec.describe CfdpPdu, type: :model do
331
338
  expect(buffer[29..32].unpack('A*')[0]).to eql 'flow'
332
339
 
333
340
  hash = {}
341
+ hash['VERSION'] = 1
334
342
  # decom takes just the Metadata specific part of the buffer
335
343
  # so start at offset 8 and ignore the 2 checksum bytes
336
344
  CfdpPdu.decom_metadata_pdu_contents(CfdpPdu.new(crcs_required: false), hash, buffer[8..-3])
@@ -343,5 +351,98 @@ RSpec.describe CfdpPdu, type: :model do
343
351
  expect(tlv['TYPE']).to eql 'FLOW_LABEL'
344
352
  expect(tlv['FLOW_LABEL']).to eql 'flow'
345
353
  end
354
+
355
+ it "builds a Metadata PDU for version 0" do
356
+ destination_entity = CfdpMib.entity(@destination_entity_id)
357
+ destination_entity['protocol_version_number'] = 0
358
+ buffer = CfdpPdu.build_metadata_pdu(
359
+ source_entity: CfdpMib.entity(@source_entity_id),
360
+ transaction_seq_num: 1,
361
+ destination_entity: destination_entity,
362
+ file_size: 0xDEADBEEF,
363
+ segmentation_control: "NOT_PRESERVED",
364
+ transmission_mode: nil,
365
+ source_file_name: "filename",
366
+ destination_file_name: "test",
367
+ closure_requested: 0,
368
+ options: [])
369
+ # puts buffer.formatted
370
+ expect(buffer.length).to eql 29
371
+
372
+ # By default the first 7 bytes are the header
373
+ # This assumes 1 byte per entity ID and sequence number
374
+ expect(buffer[1..2].unpack('n')[0]).to eql 22 # PDU_DATA_LENGTH - Directive Code plus Data plus CRC
375
+
376
+ # Directive Code
377
+ expect(buffer[7].unpack('C')[0]).to eql 7 # Metadata per Table 5-4
378
+ # Closure requested
379
+ expect(buffer[8].unpack('C')[0] >> 6).to eql 0
380
+ # Checksum type
381
+ expect(buffer[8].unpack('C')[0] & 0xF).to eql 0 # legacy modular checksum
382
+ # File Size
383
+ expect(buffer[9..12].unpack('N')[0]).to eql 0xDEADBEEF
384
+ # Source File Name
385
+ expect(buffer[13].unpack('C')[0]).to eql 8
386
+ expect(buffer[14..21].unpack('A*')[0]).to eql 'filename'
387
+ # Destination File Name
388
+ expect(buffer[22].unpack('C')[0]).to eql 4
389
+ expect(buffer[23..26].unpack('A*')[0]).to eql 'test'
390
+
391
+ hash = {}
392
+ hash['VERSION'] = 0
393
+ # decom takes just the Metadata specific part of the buffer
394
+ # so start at offset 8 and ignore the 2 checksum bytes
395
+ CfdpPdu.decom_metadata_pdu_contents(CfdpPdu.new(crcs_required: false), hash, buffer[8..-3])
396
+ expect(hash['CLOSURE_REQUESTED']).to eql nil
397
+ expect(hash['CHECKSUM_TYPE']).to eql nil
398
+ expect(hash['FILE_SIZE']).to eql 0xDEADBEEF
399
+ expect(hash['SOURCE_FILE_NAME']).to eql 'filename'
400
+ expect(hash['DESTINATION_FILE_NAME']).to eql 'test'
401
+ end
402
+
403
+ it "builds a Metadata PDU with unknown checksum type, large file size, and default closure requested" do
404
+ destination_entity = CfdpMib.entity(@destination_entity_id)
405
+ destination_entity['default_checksum_type'] = 9 # Unsupported
406
+ buffer = CfdpPdu.build_metadata_pdu(
407
+ source_entity: CfdpMib.entity(@source_entity_id),
408
+ transaction_seq_num: 1,
409
+ destination_entity: destination_entity,
410
+ file_size: 0x100000000,
411
+ segmentation_control: "NOT_PRESERVED",
412
+ transmission_mode: nil,
413
+ source_file_name: "filename",
414
+ destination_file_name: "test",
415
+ closure_requested: nil,
416
+ options: [])
417
+ # puts buffer.formatted
418
+ expect(buffer.length).to eql 33
419
+
420
+ # By default the first 7 bytes are the header
421
+ # This assumes 1 byte per entity ID and sequence number
422
+ expect(buffer[1..2].unpack('n')[0]).to eql 26 # PDU_DATA_LENGTH - Directive Code plus Data plus CRC
423
+
424
+ # Directive Code
425
+ expect(buffer[7].unpack('C')[0]).to eql 7 # Metadata per Table 5-4
426
+ # Closure requested
427
+ expect(buffer[8].unpack('C')[0] >> 6).to eql 1
428
+ # Checksum type
429
+ expect(buffer[8].unpack('C')[0] & 0xF).to eql 0 # legacy modular checksum
430
+ # File Size
431
+ expect(buffer[9..16].unpack('Q>')[0]).to eql 0x100000000
432
+ # Source File Name
433
+ expect(buffer[17].unpack('C')[0]).to eql 8
434
+ expect(buffer[18..25].unpack('A*')[0]).to eql 'filename'
435
+ # Destination File Name
436
+ expect(buffer[26].unpack('C')[0]).to eql 4
437
+ expect(buffer[27..30].unpack('A*')[0]).to eql 'test'
438
+
439
+ # Test with toplevel decom
440
+ hash = CfdpPdu.decom(buffer)
441
+ expect(hash['CLOSURE_REQUESTED']).to eql "CLOSURE_REQUESTED"
442
+ expect(hash['CHECKSUM_TYPE']).to eql 0
443
+ expect(hash['FILE_SIZE']).to eql 0x100000000
444
+ expect(hash['SOURCE_FILE_NAME']).to eql 'filename'
445
+ expect(hash['DESTINATION_FILE_NAME']).to eql 'test'
446
+ end
346
447
  end
347
448
  end