1 /*
<lambda>null2 * Copyright (C) 2025 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package com.android.platform.test.ravenwood.ravenhelper.policytoannot
17
18 /*
19 * This file contains classes and functions about file edit operations, such as
20 * "insert a line", "delete a line".
21 */
22
23
24 import com.android.hoststubgen.log
25 import java.io.BufferedWriter
26 import java.io.File
27 import java.io.FileOutputStream
28 import java.io.OutputStreamWriter
29
30 enum class SourceOperationType {
31 /** Insert a line */
32 Insert,
33
34 /** delete a line */
35 Delete,
36
37 /** Insert a text at the beginning of a line */
38 Prepend,
39 }
40
41 data class SourceOperation(
42 /** Target file to edit. */
43 val sourceFile: String,
44
45 /** 1-based line number. Use -1 to add at the end of the file. */
46 val lineNumber: Int,
47
48 /** Operation type.*/
49 val type: SourceOperationType,
50
51 /** Operand -- text to insert or prepend. Ignored for delete. */
52 val text: String = "",
53
54 /** Human-readable description of why this operation was created */
55 val description: String,
56 ) {
toStringnull57 override fun toString(): String {
58 return "SourceOperation(sourceFile='$sourceFile', " +
59 "lineNumber=$lineNumber, type=$type, text='$text' desc='$description')"
60 }
61 }
62
63 /**
64 * Stores list of [SourceOperation]s for each file.
65 */
66 class SourceOperations {
67 var size: Int = 0
68 private set
69 private val fileOperations = mutableMapOf<String, MutableList<SourceOperation>>()
70
addnull71 fun add(op: SourceOperation) {
72 log.forVerbose {
73 log.v("Adding operation: $op")
74 }
75 size++
76 fileOperations[op.sourceFile]?.let { ops ->
77 ops.add(op)
78 return
79 }
80 fileOperations[op.sourceFile] = mutableListOf(op)
81 }
82
83 /**
84 * Get the collected [SourceOperation]s for each file.
85 */
getOperationsnull86 fun getOperations(): MutableMap<String, MutableList<SourceOperation>> {
87 return fileOperations
88 }
89 }
90
91 /**
92 * Create a shell script to apply all the operations (using sed).
93 */
createShellScriptnull94 fun createShellScript(ops: SourceOperations, writer: BufferedWriter) {
95 // File header.
96 // Note ${'$'} is an ugly way to put a dollar sign ($) in a multi-line string.
97 writer.write(
98 """
99 #!/bin/bash
100
101 set -e # Finish when any command fails.
102
103 function apply() {
104 local file="${'$'}1"
105
106 # The script is given via stdin. Write it to file.
107 local sed="/tmp/pta-script.sed.tmp"
108 cat > "${'$'}sed"
109
110 echo "Running: sed -i -f \"${'$'}sed\" \"${'$'}file\""
111
112 if ! sed -i -f "${'$'}sed" "${'$'}file" ; then
113 echo 'Failed!' 1>&2
114 return 1
115 fi
116 }
117
118 """.trimIndent()
119 )
120
121 ops.getOperations().toSortedMap().forEach { (origFile, ops) ->
122 val file = File(origFile).absolutePath
123
124 writer.write("\n")
125
126 writer.write("#")
127 writer.write("=".repeat(78))
128 writer.write("\n")
129
130 writer.write("\n")
131
132 writer.write("apply \"$file\" <<'__END_OF_SCRIPT__'\n")
133 toSedScript(ops, writer)
134 writer.write("__END_OF_SCRIPT__\n")
135 }
136
137 writer.write("\n")
138
139 writer.write("echo \"All files updated successfully!\"\n")
140 writer.flush()
141 }
142
143 /**
144 * Create a sed script to apply a list of operations.
145 */
toSedScriptnull146 private fun toSedScript(ops: List<SourceOperation>, writer: BufferedWriter) {
147 ops.sortedBy { it.lineNumber }.forEach { op ->
148 if (op.text.contains('\n')) {
149 throw RuntimeException("Operation $op may not contain newlines.")
150 }
151
152 // Convert each operation to a sed operation. Examples:
153 //
154 // - Insert "abc" to line 2
155 // 2i\
156 // abc
157 //
158 // - Insert "abc" to the end of the file
159 // $a\
160 // abc
161 //
162 // - Delete line 2
163 // 2d
164 //
165 // - Prepend abc to line 2
166 // 2s/^/abc/
167 //
168 // The line numbers are all the line numbers in the original file. Even though
169 // the script itself will change them because of inserts and deletes, we don't need to
170 // change the line numbers in the script.
171
172 // Write the target line number.
173 writer.write("\n")
174 writer.write("# ${op.description}\n")
175 if (op.lineNumber >= 0) {
176 writer.write(op.lineNumber.toString())
177 } else {
178 writer.write("$")
179 }
180
181 when (op.type) {
182 SourceOperationType.Insert -> {
183 if (op.lineNumber >= 0) {
184 writer.write("i\\\n") // "Insert"
185 } else {
186 // If it's the end of the file, we need to use "a" (append)
187 writer.write("a\\\n")
188 }
189 writer.write(op.text)
190 writer.write("\n")
191 }
192 SourceOperationType.Delete -> {
193 writer.write("d\n")
194 }
195 SourceOperationType.Prepend -> {
196 if (op.text.contains('/')) {
197 TODO("Operation $op contains character(s) that needs to be escaped.")
198 }
199 writer.write("s/^/${op.text}/\n")
200 }
201 }
202 }
203 }
204
createShellScriptnull205 fun createShellScript(ops: SourceOperations, scriptFile: String?): Boolean {
206 if (ops.size == 0) {
207 log.i("No files need to be updated.")
208 return false
209 }
210
211 val scriptWriter = BufferedWriter(
212 OutputStreamWriter(
213 scriptFile?.let { file ->
214 FileOutputStream(file)
215 } ?: System.out
216 ))
217
218 scriptWriter.use { writer ->
219 scriptFile?.let {
220 log.i("Creating script file at $it ...")
221 }
222 createShellScript(ops, writer)
223 }
224 return true
225 }