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.
- checksums.yaml +4 -4
- data/README.md +10 -1
- data/microservices/CFDP/Gemfile +3 -11
- data/microservices/CFDP/Gemfile.dev +35 -0
- data/microservices/CFDP/Gemfile.prod +29 -0
- data/microservices/CFDP/app/controllers/cfdp_controller.rb +8 -0
- data/microservices/CFDP/app/models/cfdp_crc_checksum.rb +3 -1
- data/microservices/CFDP/app/models/cfdp_mib.rb +2 -1
- data/microservices/CFDP/app/models/cfdp_pdu.rb +43 -19
- data/microservices/CFDP/app/models/cfdp_receive_transaction.rb +7 -2
- data/microservices/CFDP/app/models/cfdp_source_transaction.rb +6 -1
- data/microservices/CFDP/app/models/cfdp_transaction.rb +1 -1
- data/microservices/CFDP/app/models/cfdp_user.rb +14 -4
- data/microservices/CFDP/config/application.rb +0 -11
- data/microservices/CFDP/config/environments/test.rb +1 -1
- data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_file_data.rb +6 -2
- data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_finished.rb +17 -5
- data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_metadata.rb +28 -11
- data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_tlv.rb +5 -2
- data/microservices/CFDP/lib/cfdp_pdu/cfdp_pdu_user_ops.rb +16 -9
- data/microservices/CFDP/spec/controllers/cfdp_controller_spec.rb +373 -0
- data/microservices/CFDP/spec/models/cfdp_crc_checksum_spec.rb +134 -0
- data/microservices/CFDP/spec/models/cfdp_mib_spec.rb +389 -0
- data/microservices/CFDP/spec/models/cfdp_pdu_metadata_spec.rb +102 -1
- data/microservices/CFDP/spec/models/cfdp_pdu_user_ops_spec.rb +562 -0
- data/microservices/CFDP/spec/models/cfdp_receive_transaction_spec.rb +598 -0
- data/microservices/CFDP/spec/models/cfdp_transaction_spec.rb +331 -0
- data/microservices/CFDP/spec/models/cfdp_user_spec.rb +575 -0
- 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
|
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
|