File Coverage

lib/CodeGen/Protection.pm
Criterion Covered Total %
statement 32 32 100.0
branch n/a
condition n/a
subroutine 11 11 100.0
pod 2 2 100.0
total 45 45 100.0


line stmt bran cond sub pod time code
1             package CodeGen::Protection;
2              
3             # ABSTRACT: Safely rewrite parts of generated code
4              
5 3     3   395371 use v5.10.0; # for named captures in regexes
  3         42  
6 3     3   16 use strict;
  3         5  
  3         75  
7 3     3   14 use warnings;
  3         4  
  3         75  
8 3     3   15 use base 'Exporter';
  3         4  
  3         293  
9 3     3   1624 use Module::Runtime qw( use_module );
  3         5571  
  3         17  
10 3     3   220 use Carp 'croak';
  3         5  
  3         154  
11 3         43 use CodeGen::Protection::Types qw(
12             compile_named
13             NonEmptyStr
14             Bool
15             Optional
16 3     3   1051 );
  3         10  
17              
18             our $VERSION = '0.02';
19             our @EXPORT_OK = qw(
20             create_protected_code
21             rewrite_code
22             );
23             our %EXPORT_TAGS = ( all => \@EXPORT_OK );
24              
25             sub create_protected_code {
26 3     3 1 280 state $check = compile_named(
27             type => NonEmptyStr,
28             protected_code => NonEmptyStr,
29             tidy => Optional [Bool],
30             name => Optional [NonEmptyStr],
31             overwrite => Optional [Bool],
32             );
33 3         11256 return _rewritten( $check->(@_) );
34             }
35              
36             sub rewrite_code {
37 9     9 1 18125 state $check = compile_named(
38             type => NonEmptyStr,
39             protected_code => NonEmptyStr,
40             existing_code => NonEmptyStr,
41             tidy => Optional [Bool],
42             name => Optional [NonEmptyStr],
43             overwrite => Optional [Bool],
44             );
45 9         10435 return _rewritten( $check->(@_) );
46             }
47              
48             sub _rewritten {
49 12     12   457 my $arg_for = shift;
50 12         28 my $type = delete $arg_for->{type};
51 12         35 my $class = _use_module($type);
52 12         275 return $class->new($arg_for)->rewritten;
53             }
54              
55             sub _use_module {
56 12     12   21 my $type = shift;
57 12         36 my $class = "CodeGen::Protection::Format::$type";
58 12         52 use_module($class);
59 12         379 return $class;
60             }
61              
62             1;
63              
64             __END__
65              
66             =pod
67              
68             =encoding UTF-8
69              
70             =head1 NAME
71              
72             CodeGen::Protection - Safely rewrite parts of generated code
73              
74             =head1 VERSION
75              
76             version 0.02
77              
78             =head1 SYNOPSIS
79              
80             use CodeGen::Protection qw(:all);
81              
82             # Creating a new document:
83              
84             my $perl = create_protected_code(
85             type => 'Perl',
86             protected_code => $sample,
87             );
88              
89             # Or rewriting:
90              
91             my $rewritten = rewrite_code(
92             type => 'Perl',
93             existing_code => $perl,
94             protected_code => $rewritten_code,
95             );
96              
97             =head1 DESCRIPTION
98              
99             Code that writes code can be a powerful tool, especially when you need to
100             generate lots of boilerplate. However, when a developer takes the generated
101             code, they can easily rewrite that code in a way that no longer works, or make
102             good changes that get wiped out if the code is regenerated.
103             L<https://metacpan.org/pod/DBIx::Class::Schema::Loader> protects against this
104             by marking blocks of code with start and end comments and an MD5 checksum. If
105             you change any of the code between those comments, regenerating your schema
106             will fail.
107              
108             This module takes this idea and generalizes it. It allows you to do a safe
109             partial rewrite of documents. At the present time, we support Perl and HTML.
110              
111             In short, we wrap your "protected" (C<protected_code>) code in start and end
112             comments, with checksums for the code:
113              
114             #<<< CodeGen::Protection::Perl 0.01. Do not touch any code between this and the end comment. Checksum: fa97a021bd70bf3b9fa3e52f203f2660
115            
116             # protected code goes here
117              
118             #>>> CodeGen::Protection::Perl 0.01. Do not touch any code between this and the start comment. Checksum: fa97a021bd70bf3b9fa3e52f203f2660
119              
120             Or:
121              
122             <!-- CodeGen::Protection::Format::HTML 0.01. Do not touch any code between this and the end comment. Checksum: c286b9b2577e085df857227eae996c40 -->
123            
124             <ol>
125             <li>This is a list</li>
126             <li>This is the second entry.</li>
127             </ol>
128            
129             <!-- CodeGen::Protection::Format::HTML 0.01. Do not touch any code between this and the start comment. Checksum: c286b9b2577e085df857227eae996c40 -->
130              
131             If calling the C<rewrite_code> function, this module removes the code between
132             the C<existing_code>'s start and end markers and replaces it with the
133             C<protected_code>. If the code between the start and end markers has been
134             altered, it will no longer match the checksums and rewriting the code will
135             fail.
136              
137             =head1 TYPES
138              
139             As of this writing, we can protect Perl and HTML:
140              
141             my $rewritten = rewrite_code(
142             type => 'Perl',
143             existing_code => $perl,
144             protected_code => $protected_code,
145             );
146              
147             my $rewritten = rewrite_code(
148             type => 'HTML',
149             existing_code => $HTML,
150             protected_code => $protected_code,
151             );
152              
153             See L<CodeGen::Protection::Role> to learn how to create your own types to protect.
154              
155             =head1 FUNCTIONS
156              
157             Functions are exportable on-demand, or both can be exported via C<:all>.
158              
159             use CodeGen::Protection qw(rewrite_code);
160             use CodeGen::Protection qw(:all);
161              
162             =head2 C<create_protected_code>
163              
164             my $protected_code = create_protected_code(
165             type => 'Perl',
166             protected_code => $text_of_code,
167             );
168              
169             Takes the code in C<$text_of_code> and adds start and end markers to it.
170              
171             =head2 C<rewrite_code>
172              
173             my $protected_code = create_protected_code(
174             type => 'Perl',
175             protected_code => $protected_code,
176             existing_code => $existing_code,
177             );
178              
179             Replaces the code in the protected block of C<$existing_code> with the code
180             from C<$protected_code>.
181              
182             =head3 ARGUMENTS
183              
184             Both C<create_protected_code> and C<rewrite_code> take the same arguments,
185             except that C<rewrite_code> does not allow the C<protected_code> argument.
186              
187             =over 4
188              
189             =item * C<protected_code>
190              
191             This is a required string containing any new Perl code to be built with this
192             tool.
193              
194             =item * C<existing_code>
195              
196             This is an optional string containing Perl code already built with this tool.
197             If provided, this code I<must> have the start and end markers generated by
198             this tool so that the rewriter knows the section of code to replace with the
199             injected code.
200              
201             =item * C<name>
202              
203             Optional name for the code. This is only used in error messages if you're
204             generating a lot of code and an error occurs and you'd like to see the name
205             in the error.
206              
207             =item * C<tidy>
208              
209             If true, will attempt to tidy the C<protected_code> block (the rest of the
210             code is ignored). For Perl, if the value of perltidy is the number 1 (one),
211             then a generic pass of L<Perl::Tidy> will be done on the code. If the value is
212             true and anything I<other> than one, this is assumed to be the path to a
213             F<.perltidyrc> file and that will be used to tidy the code (or C<croak()> if
214             the F<.perltidyrc> file cannot be found).
215              
216             =item * C<overwrite>
217              
218             Optional boolean, default false. In "Rewrite mode", if the checksum in the
219             start and end markers doesn't match the code within them, someone has manually
220             altered that code and we do not automatically overwrite it (in fact, we
221             C<croak()>). Setting C<overwrite> to true will cause it to be overwritten.
222              
223             =back
224              
225             =head1 MODES
226              
227             There are two modes: "Creation" and "Rewrite."
228              
229             =head2 Creation Mode
230              
231             my $rewrite = CodeGen::Protection::Perl->new(
232             protected_code => $text,
233             );
234             say $rewrite->rewritten;
235              
236             If you create an instance with C<protected_code> but not old text, this will wrap
237             the new text in start and end tags that "protect" the document if you rewrite
238             it:
239              
240             my $perl = <<'END';
241             sub sum {
242             my $total = 0;
243             $total += $_ foreach @_;
244             return $total;
245             }
246             END
247             my $rewrite = CodeGen::Protection::Perl->new( protected_code => $perl );
248             say $rewrite->rewritten;
249              
250             Output:
251              
252             #<<< CodeGen::Protection::Perl 0.01. Do not touch any code between this and the end comment. Checksum: fa97a021bd70bf3b9fa3e52f203f2660
253              
254             sub sum {
255             my $total = 0;
256             $total += $_ foreach @_;
257             return $total;
258             }
259              
260             #>>> CodeGen::Protection::Perl 0.01. Do not touch any code between this and the start comment. Checksum: fa97a021bd70bf3b9fa3e52f203f2660
261              
262             You can then take the marked up document and insert it into another Perl
263             document and use the rewrite mode to safely rewrite the code between the start
264             and end markers. The rest of the document will be ignored.
265              
266             Note that leading and trailing comments start with C<< #<<< >> and C<< #>>> >>
267             respectively. Those are special comments which tell L<Perl::Tidy> to ignore
268             what ever is between them. Thus, you can safely tidy code written with this.
269              
270             The start and end checksums are the same and are the checksum of the text
271             between the comments. Leading and trailing lines which are all whitespace are
272             removed and one leading and one trailing newline will be added.
273              
274             =head2 Rewrite Mode
275              
276             Given a document created with the "Creating" mode, you can then take the
277             marked up document and insert it into another Perl document and use the
278             rewrite mode to safely rewrite the code between the start and end markers.
279             The rest of the document will be ignored.
280              
281             my $rewrite = CodeGen::Protection::Perl->new(
282             existing_code => $existing_code,
283             protected_code => $protected_code,
284             );
285             say $rewrite->rewritten;
286              
287             In the above, assuming that C<$existing_code> is a rewritable document, the
288             C<$protected_code> will replace the rewritable section of the C<$existing_code>, leaving
289             the rest unchanged.
290              
291             However, if C<$protected_code> is I<also> a rewritable document, then the rewritable
292             portion of the C<$protected_code> will be extract and used to replace the rewritable
293             portion of the C<$existing_code>.
294              
295             So for the code shown in the "Creation mode" section, you could add more code
296             like this:
297              
298             package My::Package;
299              
300             use strict;
301             use warnings;
302              
303             sub average {
304             return sum(@_)/@_;
305             }
306              
307             #<<< CodeGen::Protection::Perl 0.01. Do not touch any code between this and the end comment. Checksum: fa97a021bd70bf3b9fa3e52f203f2660
308              
309             sub sum {
310             my $total = 0;
311             $total += $_ foreach @_;
312             return $total;
313             }
314              
315             #>>> CodeGen::Protection::Perl 0.01. Do not touch any code between this and the start comment. Checksum: fa97a021bd70bf3b9fa3e52f203f2660
316            
317             1;
318              
319             However, later on I might realize that the C<sum> function will happily try to
320             sum things which are not numbers, so I want to fix that. I'll slurp the C<My::Package> code
321             into the C<$existing_code> variable and then:
322              
323             my $perl = <<'END';
324             use Scalar::Util 'looks_like_number';
325              
326             sub sum {
327             my $total = 0;
328             foreach my $number (@_) {
329             unless (looks_like_number($number)) {
330             die "'$number' doesn't look like a numbeer!";
331             }
332             $total += $number;
333             }
334             return $total;
335             }
336             END
337             my $rewrite = CodeGen::Protection::Perl->new( existing_code => $existing_code, protected_code => $perl );
338             say $rewrite->rewritten;
339              
340             And that will print out:
341              
342             package My::Package;
343            
344             use strict;
345             use warnings;
346            
347             sub average {
348             return sum(@_)/@_;
349             }
350            
351             #<<< CodeGen::Protection::Perl 0.01. Do not touch any code between this and the end comment. Checksum: d135a051f158ee19fbd68af5466fb1ae
352            
353             use Scalar::Util 'looks_like_number';
354            
355             sub sum {
356             my $total = 0;
357             foreach my $number (@_) {
358             unless (looks_like_number($number)) {
359             die "'$number' doesn't look like a numbeer!";
360             }
361             $total += $number;
362             }
363             return $total;
364             }
365            
366             #>>> CodeGen::Protection::Perl 0.01. Do not touch any code between this and the start comment. Checksum: d135a051f158ee19fbd68af5466fb1ae
367            
368             1;
369              
370             You can see that the code between the start and end checksum comments and been
371             rewritten, while the rest of the code remains unchanged.
372              
373             =head1 ACKNOWLEDGEMENTS
374              
375             We would like to thank L<All Around the World|https://allaroundtheworld.fr/>
376             for sponsoring this work.
377              
378             =head1 AUTHOR
379              
380             Curtis "Ovid" Poe <ovid@allaroundtheworld.fr>
381              
382             =head1 COPYRIGHT AND LICENSE
383              
384             This software is copyright (c) 2021 by Curtis "Ovid" Poe.
385              
386             This is free software; you can redistribute it and/or modify it under
387             the same terms as the Perl 5 programming language system itself.
388              
389             =cut