LinkedOps
Dissecting Serialized Linked List Operations.
Last updated
Dissecting Serialized Linked List Operations.
Last updated
We will write a seeking algorithm that takes in the file data.bin
, which contains a series of instructions that need to be performed on a Linked List. The flag can be printed by reading the list after performing the operations.
You have been provided data.bin
which contains the serialized array. Each element contains an operational code (OP) that tells what operation to perform, which in turn has the following form:
0
: Add a node to the end of the list. Following this sequence is a char
value to add.
1
: Add a node to the front of the list. Following this sequence is a char
value to add.
2
: Add a node to a specific index. Following the sequence is the index to add at, and then a char
value to add.
3
: Remove a node from the end of the list.
4
: Remove a node from the front of the list.
5
: Remove a node from a specific index. Following the sequence is the index to remove from.
The flag is generated by performing the sequence of operations provided in the data.bin
file and printing out the Linked List once the operations have been completed.
The flag will be represented in ASCII. Surround the data that you get in flag{}
braces.
This challenge is a bit different than the previous one. Rather than the serialization representing a raw data structure, we are provided raw operations to generate our own.
For this challenge, we must define how we will build the linked list, then build the linked list, and then print it once it's fully initialized.
Despite the serialized data being operations on a linked list, we still must define a linked list. The linked list holds characters. Each item must contain a pointer to the next node. We can represent this as a struct containing these two values:
From now on, we can simply refer to the struct ArrayItem
as node
.
We will open the file as always and ensure the file has been opened.
After this, we must initialize the data structure. We define the head node as NULL
indicating that the list is empty.
Now, we can begin the while
loop. Counting the number of elements before the loop is impractical because each element has a different size. It's easier to simply run an infinite loop that breaks when no more elements are read. We can check when reading the opcode
.
Why do we only need to check when we read the opcode
?
We are guaranteed by the challenge description that we have complete instances of the data structure. This means that if we read a valid opcode
, the rest of the structure is guaranteed to follow.
For each entry, we first need to find the opcode
. We can break the loop if we don't read any bytes at this step.
Our next step depends on the opcode
. You can either use a series of if
statements or a switch
to handle the opcode
. I chose if
statements because that's the first thing that comes to mind when I build solutions.
Let's handle each case:
Case 0
: Add a node to the end of the list. We need to read one more byte (the character to insert) and then insert this node at the end of the list. We also must increment the number of elements.
Case 1
: Insert at the front of the list. We need to read one more byte, being the character to insert.
Case 2
: Insert at an index. We need to read one byte for the index and one byte for the character. Then, we can insert.
Case 3
: Remove a node from the end of the list. We don't need any more data. We remove the node and then decrement the number of elements.
Case 4
: Remove a node from the front of the list. We don't need any more data. We remove the node and then decrement the number of elements.
Case 5
: Remove a node from an index. We need to read one byte for the index. We remove the node and then decrement the number of elements.
Default Case: We shouldn't ever reach here. If we read this, we should print it out and exit. This means we read something wrong.
Let's handle the insertNode
and removeNode
methods. These methods are standard linked list operations. You could quickly look online for linked list implementations, but I'll explain my own. Rather than making separate functions for inserting and removing at the start, end, and index, it's easier to use the same method. It makes the data reading code a lot simpler.
Here are the declarations for both methods:
Inserting a Node
To insert a node, we must make a new node for it. I'll dynamically allocate it to ensure it doesn't lose scope and get deleted.
We must handle the edge cases. In the case that we're inserting to an empty list (meaning head == NULL
), we can simply set the head to the new node.
We can actually combine this with the other edge case if we insert into the front. Both these have the same methodology.
Now, we can handle the main case: inserting in the center or end. We first must iterate to the location we want to insert. We want to stop at the node before the location we want to insert. At the same time, we should ensure that we aren't inserting beyond the length of the list.
Finally, once we have the location, we link in new
after next
.
Removing a Node
Removing a node is a similar process. We first handle the two edge cases: (1) when the list is already empty, or (2) when we're removing from the start.
If the list is already empty, we do nothing.
If we remove it from the start, we simply set the head to the next node and free the old head.
Then, we cover the main case: removing from the center. We navigate to the node before the one we want to remove using the same logic as inserting a node.
Once we're here, we can remove the node by linking the surrounding nodes.
This is it! Now, we can print the flag.
We can make a function that prints a linked list for us. We'll encapsulate the flag braces at the same time.
Running this prints the flag:
Here is the full solution: