Skip to content

Commit 1e27e60

Browse files
committed
Initial release 1.0.0: Complete NBT parsing and GZip support
1 parent 094aeca commit 1e27e60

File tree

10 files changed

+567
-8
lines changed

10 files changed

+567
-8
lines changed

.gitignore

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
1+
# Xcode
12
.DS_Store
2-
/.build
3-
/Packages
3+
*.swp
4+
*.lock
5+
*~.nib
6+
*.pbxuser
7+
*.mode1v3
8+
*.mode2v3
9+
*.perspectivev3
410
xcuserdata/
511
DerivedData/
6-
.swiftpm/configuration/registries.json
7-
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8-
.netrc
12+
build/
13+
*.moved-aside
14+
*.xccheckout
15+
*.xcworkspace
16+
!default.xcworkspace
17+
18+
# Swift Package Manager
19+
.build/
20+
.swiftpm/
21+
22+
# CocoaPods
23+
Pods/

Package.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ let package = Package(
1616
// Targets are the basic building blocks of a package, defining a module or a test suite.
1717
// Targets can depend on other targets in this package and products from dependencies.
1818
.target(
19-
name: "SwiftNBT"
19+
name: "SwiftNBT",
20+
linkerSettings: [
21+
.linkedLibrary("z")
22+
]
2023
),
2124
.testTarget(
2225
name: "SwiftNBTTests",

README.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<div align="center">
2+
3+
![SwiftNBT Banner](https://placehold.co/800x200/F06292/FFFFFF?text=SwiftNBT&font=montserrat)
4+
5+
![Swift](https://img.shields.io/badge/Swift-6.2+-orange?logo=swift&logoColor=white)
6+
![Platform](https://img.shields.io/badge/Platform-iOS%20|%20macOS%20|%20Linux-lightgrey)
7+
![License](https://img.shields.io/badge/License-MIT-yellow?logo=opensourceinitiative)
8+
9+
</div>
10+
11+
> ⚠️ **Compression & Linkage Notice**
12+
>
13+
> SwiftNBT utilizes the system native **zlib** library for maximum performance when handling GZip/Zlib compressed data (standard for Minecraft).
14+
>
15+
> **For Linux/Server-Side Swift:** Ensure `zlib1g-dev` is installed on your machine.
16+
> **For iOS/macOS:** It works out of the box using the system SDKs.
17+
18+
**SwiftNBT** is a lightweight, zero-dependency (other than system zlib) parser for the **Named Binary Tag (NBT)** format used by Minecraft. It is designed to be robust, type-safe, and incredibly easy to use with modern Swift syntax.
19+
20+
---
21+
22+
## How to Use: Installation
23+
24+
### Swift Package Manager (SPM)
25+
26+
You can add SwiftNBT to your project via Xcode or your `Package.swift` file.
27+
28+
**In `Package.swift`:**
29+
30+
```swift
31+
dependencies: [
32+
.package(url: "https://github.com/arnaudrmt/SwiftNBT.git", from: "1.0.0")
33+
],
34+
targets: [
35+
.target(
36+
name: "YourTarget",
37+
dependencies: ["SwiftNBT"]
38+
)
39+
]
40+
```
41+
42+
**In Xcode:**
43+
1. Go to **File > Add Package Dependencies...**
44+
2. Paste the repository URL.
45+
3. Select the version and add it to your target.
46+
47+
---
48+
49+
## API Usage Examples
50+
51+
### 1. Parsing Raw Data (Base64 or Bytes)
52+
53+
The most common use case is decoding a Base64 string that contains GZipped NBT data.
54+
55+
```swift
56+
import SwiftNBT
57+
58+
let base64String = "H4sIAAAAAAAA/..." // Your data
59+
guard let data = Data(base64Encoded: base64String) else { return }
60+
61+
do {
62+
// The parser automatically handles GZip decompression
63+
let rootTag = try NBTParser.parse(data)
64+
65+
print("Parsing successful!")
66+
rootTag.printTree() // Debug helper to see the structure
67+
} catch {
68+
print("Error: \(error)")
69+
}
70+
```
71+
72+
### 2. Accessing Data (The Easy Way)
73+
74+
Forget large `switch` statements. Use the built-in optional helpers and subscripts to navigate the NBT tree effortlessly.
75+
76+
```swift
77+
// Accessing a nested path: root -> tag -> display -> Name
78+
if let itemName = rootTag["tag"]?["display"]?["Name"]?.string {
79+
print("Item Name: \(itemName)")
80+
}
81+
82+
// Accessing lists and arrays
83+
if let inventoryList = rootTag["i"]?.array {
84+
for (index, item) in inventoryList.enumerated() {
85+
let count = item["Count"]?.byte ?? 0
86+
let id = item["id"]?.short ?? 0
87+
88+
print("Slot \(index): ID \(id) x\(count)")
89+
}
90+
}
91+
```
92+
93+
### 3. Extracting Values
94+
95+
SwiftNBT provides computed properties to safely cast values.
96+
97+
```swift
98+
let tag: NBTTag = ...
99+
100+
// Safe casting (returns nil if type mismatches)
101+
let myInt = tag.int // Works for Byte, Short, and Int types
102+
let myDouble = tag.double // Works for Float and Double types
103+
let myString = tag.string
104+
```
105+
106+
---
107+
108+
## Features
109+
110+
* **Auto-Decompression:** Automatically detects and handles GZip and ZLib headers using the native system `zlib`, ensuring 100% compatibility with Minecraft data (RFC 1952).
111+
* **Type Safety:** Built on a comprehensive `NBTTag` enum that strictly represents every Minecraft data type (Byte, Short, Int, Long, Float, Double, Arrays, Lists, Compounds).
112+
* **Syntactic Sugar:** Navigate complex NBT trees effortlessly using dictionary-like subscripts (e.g., `tag["display"]["Name"]`) and safe optional casting.
113+
* **Debug Tools:** Includes a handy `.printTree()` method to visualize the NBT structure hierarchy in the console.
114+
* **Lightweight:** Zero external dependencies (other than system libraries), keeping your build times fast and binary size small.
115+
116+
---
117+
118+
## Contributing & Maintenance
119+
120+
This library was built to provide a stable, long-term solution for the Swift Minecraft community. **It is here to stay and will be actively maintained.**
121+
122+
We welcome contributions!
123+
* **Found a bug?** Please open an [Issue](https://github.com/arnaudrmt/SwiftNBT/issues).
124+
* **Have an improvement?** Feel free to fork the repo and submit a Pull Request.
125+
126+
You don't have to worry about this library becoming deprecated or deleted; we depend on it for our own production apps.
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//
2+
// File.swift
3+
// SwiftNBT
4+
//
5+
// Created by Arnaud on 28/11/2025.
6+
//
7+
8+
import Foundation
9+
10+
/// The main entry point for parsing NBT Data.
11+
public class NBTParser {
12+
13+
/// Parses raw NBT data into an `NBTTag` structure.
14+
/// Automatically handles GZip decompression if needed.
15+
///
16+
/// - Parameter data: The raw data (can be Base64 decoded data, or raw bytes).
17+
/// - Returns: The root `NBTTag` (usually a Compound).
18+
/// - Throws: `NBTError` if parsing or decompression fails.
19+
public static func parse(_ data: Data) throws -> NBTTag {
20+
// 1. Attempt automatic GZip decompression
21+
let rawData = try data.gunzipped()
22+
23+
// 2. Start parsing stream
24+
let stream = NBTStream(data: rawData)
25+
return try parseRoot(stream: stream)
26+
}
27+
28+
// MARK: - Internal Parsing Logic
29+
30+
private static func parseRoot(stream: NBTStream) throws -> NBTTag {
31+
let typeId = try stream.readByte()
32+
33+
if typeId == 0 { return .end } // Empty file
34+
35+
// Root is always a named compound (ID 10)
36+
if typeId != 10 {
37+
throw NBTError.invalidFormat
38+
}
39+
40+
// Root tags have a name (often empty string), we consume it but don't store it here
41+
_ = try stream.readString()
42+
43+
return try readTagPayload(id: 10, stream: stream)
44+
}
45+
46+
private static func readTagPayload(id: UInt8, stream: NBTStream) throws -> NBTTag {
47+
switch id {
48+
case 1: return .byte(try stream.readInt8())
49+
case 2: return .short(try stream.readInt16())
50+
case 3: return .int(try stream.readInt32())
51+
case 4: return .long(try stream.readInt64())
52+
case 5: return .float(try stream.readFloat())
53+
case 6: return .double(try stream.readDouble())
54+
55+
case 7: // Byte Array
56+
let length = try stream.readInt32()
57+
var bytes: [Int8] = []
58+
// Optimization: could read bytes in bulk, but loop is safer for endianness
59+
for _ in 0..<length { bytes.append(try stream.readInt8()) }
60+
return .byteArray(bytes)
61+
62+
case 8: return .string(try stream.readString())
63+
64+
case 9: // List
65+
let elementTypeId = try stream.readByte()
66+
let length = try stream.readInt32()
67+
var list: [NBTTag] = []
68+
for _ in 0..<length {
69+
list.append(try readTagPayload(id: elementTypeId, stream: stream))
70+
}
71+
return .list(list)
72+
73+
case 10: // Compound
74+
var dict: [String: NBTTag] = [:]
75+
while true {
76+
let nextId = try stream.readByte()
77+
if nextId == 0 { break } // Tag_End
78+
79+
let name = try stream.readString()
80+
let value = try readTagPayload(id: nextId, stream: stream)
81+
dict[name] = value
82+
}
83+
return .compound(dict)
84+
85+
case 11: // Int Array
86+
let length = try stream.readInt32()
87+
var ints: [Int32] = []
88+
for _ in 0..<length { ints.append(try stream.readInt32()) }
89+
return .intArray(ints)
90+
91+
case 12: // Long Array
92+
let length = try stream.readInt32()
93+
var longs: [Int64] = []
94+
for _ in 0..<length { longs.append(try stream.readInt64()) }
95+
return .longArray(longs)
96+
97+
default:
98+
throw NBTError.unknownTagId(id)
99+
}
100+
}
101+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//
2+
// File.swift
3+
// SwiftNBT
4+
//
5+
// Created by Arnaud on 28/11/2025.
6+
//
7+
8+
import Foundation
9+
10+
/// Internal utility class to read binary data sequentially (Big Endian).
11+
internal class NBTStream {
12+
private let data: Data
13+
private var offset: Int = 0
14+
15+
init(data: Data) {
16+
self.data = data
17+
}
18+
19+
// MARK: - Core Reading
20+
21+
func readByte() throws -> UInt8 {
22+
guard offset < data.count else { throw NBTError.endOfStream }
23+
let b = data[offset]
24+
offset += 1
25+
return b
26+
}
27+
28+
func readBytes(_ count: Int) throws -> Data {
29+
guard offset + count <= data.count else { throw NBTError.endOfStream }
30+
let sub = data.subdata(in: offset..<(offset + count))
31+
offset += count
32+
return sub
33+
}
34+
35+
// MARK: - Primitives (Big Endian)
36+
37+
func readInt8() throws -> Int8 {
38+
return Int8(bitPattern: try readByte())
39+
}
40+
41+
func readInt16() throws -> Int16 {
42+
let data = try readBytes(2)
43+
return Int16(bigEndian: data.withUnsafeBytes { $0.load(as: Int16.self) })
44+
}
45+
46+
func readInt32() throws -> Int32 {
47+
let data = try readBytes(4)
48+
return Int32(bigEndian: data.withUnsafeBytes { $0.load(as: Int32.self) })
49+
}
50+
51+
func readInt64() throws -> Int64 {
52+
let data = try readBytes(8)
53+
return Int64(bigEndian: data.withUnsafeBytes { $0.load(as: Int64.self) })
54+
}
55+
56+
func readFloat() throws -> Float {
57+
let bits = try readInt32()
58+
return Float(bitPattern: UInt32(bitPattern: bits))
59+
}
60+
61+
func readDouble() throws -> Double {
62+
let bits = try readInt64()
63+
return Double(bitPattern: UInt64(bitPattern: bits))
64+
}
65+
66+
func readString() throws -> String {
67+
let length = try readInt16()
68+
let data = try readBytes(Int(length))
69+
guard let str = String(data: data, encoding: .utf8) else {
70+
throw NBTError.invalidString
71+
}
72+
return str
73+
}
74+
}

Sources/SwiftNBT/Core/NBTTag.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//
2+
// File.swift
3+
// SwiftNBT
4+
//
5+
// Created by Arnaud on 28/11/2025.
6+
//
7+
8+
/// Represents a Minecraft Named Binary Tag (NBT).
9+
/// Reference: https://wiki.vg/NBT
10+
public enum NBTTag: Equatable {
11+
case end
12+
case byte(Int8)
13+
case short(Int16)
14+
case int(Int32)
15+
case long(Int64)
16+
case float(Float)
17+
case double(Double)
18+
case byteArray([Int8])
19+
case string(String)
20+
case list([NBTTag])
21+
case compound([String: NBTTag])
22+
case intArray([Int32])
23+
case longArray([Int64])
24+
25+
/// The official numeric ID of the tag used in the binary format.
26+
internal var id: UInt8 {
27+
switch self {
28+
case .end: return 0
29+
case .byte: return 1
30+
case .short: return 2
31+
case .int: return 3
32+
case .long: return 4
33+
case .float: return 5
34+
case .double: return 6
35+
case .byteArray: return 7
36+
case .string: return 8
37+
case .list: return 9
38+
case .compound: return 10
39+
case .intArray: return 11
40+
case .longArray: return 12
41+
}
42+
}
43+
}

0 commit comments

Comments
 (0)